@mapcreator/api 5.0.0-alpha.87 → 5.0.0-alpha.89
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/cjs/api/choropleth.d.ts +4 -81
- package/cjs/api/choropleth.d.ts.map +1 -1
- package/cjs/api/choropleth.js +17 -26
- package/cjs/api/choropleth.js.map +1 -1
- 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/jobRevision.d.ts.map +1 -1
- package/cjs/api/jobRevision.js +4 -6
- package/cjs/api/jobRevision.js.map +1 -1
- package/cjs/api/mapstyleSet.d.ts +2 -1
- package/cjs/api/mapstyleSet.d.ts.map +1 -1
- package/cjs/api/mapstyleSet.js +9 -0
- package/cjs/api/mapstyleSet.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/choropleth.d.ts +4 -81
- package/esm/api/choropleth.d.ts.map +1 -1
- package/esm/api/choropleth.js +16 -23
- package/esm/api/choropleth.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/jobRevision.d.ts.map +1 -1
- package/esm/api/jobRevision.js +4 -6
- package/esm/api/jobRevision.js.map +1 -1
- package/esm/api/mapstyleSet.d.ts +2 -1
- package/esm/api/mapstyleSet.d.ts.map +1 -1
- package/esm/api/mapstyleSet.js +8 -0
- package/esm/api/mapstyleSet.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 +1 -1
- package/src/api/choropleth.ts +46 -142
- package/src/api/font.ts +9 -1
- package/src/api/insetMap.ts +4 -5
- package/src/api/jobRevision.ts +4 -5
- package/src/api/mapstyleSet.ts +23 -1
- package/src/oauth.ts +28 -258
- package/src/utils.ts +4 -4
package/src/api/choropleth.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import { type ApiError, type ApiSuccess,
|
|
2
|
-
|
|
3
|
-
import type { RequireAtLeastOne } from 'type-fest';
|
|
1
|
+
import { type ApiError, type ApiSuccess, Flatten, Revivers, getSearchParams, request } from '../utils.js';
|
|
4
2
|
import type { Polygon } from 'geojson';
|
|
5
|
-
|
|
6
|
-
export const boundingBoxRevivers: Revivers<
|
|
7
|
-
{ data: { bounding_box: string } } & Omit<ApiSuccess, 'data'> | ApiError,
|
|
8
|
-
{ boundingBox: Polygon }
|
|
9
|
-
> = {
|
|
10
|
-
boundingBox: (data: { bounding_box: string }) => JSON.parse(data.bounding_box) as Polygon,
|
|
11
|
-
};
|
|
3
|
+
import { RequireAtLeastOne } from 'type-fest';
|
|
12
4
|
|
|
13
5
|
export type ApiSearchPoint = {
|
|
14
6
|
lat: number;
|
|
@@ -22,6 +14,23 @@ export type ApiSearchBounds = {
|
|
|
22
14
|
max_lng: number;
|
|
23
15
|
};
|
|
24
16
|
|
|
17
|
+
type ApiSingleOrGroupedArea = {
|
|
18
|
+
id: number;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle: string;
|
|
21
|
+
svg_preview: string;
|
|
22
|
+
bounding_box: string;
|
|
23
|
+
is_group: boolean;
|
|
24
|
+
vector_source: string | null;
|
|
25
|
+
source_layer: string | null;
|
|
26
|
+
feature_id: number | null;
|
|
27
|
+
properties: Record<string, string> | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ApiSingleOrGroupedAreaArray = {
|
|
31
|
+
data: ApiSingleOrGroupedArea[];
|
|
32
|
+
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
33
|
+
|
|
25
34
|
type SingleOrGroupedAreaBase = {
|
|
26
35
|
id: number;
|
|
27
36
|
title: string;
|
|
@@ -38,51 +47,14 @@ export type GroupedArea = SingleOrGroupedAreaBase & {
|
|
|
38
47
|
// TODO don't export this once search on click is out
|
|
39
48
|
export type SingleArea = SingleOrGroupedAreaBase & {
|
|
40
49
|
isGroup: false;
|
|
41
|
-
properties: Record<string, string>;
|
|
42
50
|
vectorSource: string;
|
|
43
51
|
sourceLayer: string;
|
|
44
52
|
featureId: number;
|
|
53
|
+
properties: Record<string, string>;
|
|
45
54
|
};
|
|
46
55
|
|
|
47
|
-
export type SingleOrGroupedArea =
|
|
48
|
-
id: number;
|
|
49
|
-
title: string;
|
|
50
|
-
subtitle: string;
|
|
51
|
-
svgPreview: string;
|
|
52
|
-
boundingBox: Polygon;
|
|
53
|
-
isGroup: boolean;
|
|
54
|
-
properties: Record<string, string> | null;
|
|
55
|
-
vectorSource: string | null;
|
|
56
|
-
sourceLayer: string | null;
|
|
57
|
-
featureId: number | null;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export type ApiSingleOrGroupedArea = {
|
|
61
|
-
data: {
|
|
62
|
-
id: number;
|
|
63
|
-
title: string;
|
|
64
|
-
subtitle: string;
|
|
65
|
-
svg_preview: string;
|
|
66
|
-
bounding_box: string;
|
|
67
|
-
is_group: boolean;
|
|
68
|
-
properties: string | null;
|
|
69
|
-
vector_source: string | null;
|
|
70
|
-
source_layer: string | null;
|
|
71
|
-
feature_id: number | null;
|
|
72
|
-
};
|
|
73
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
74
|
-
|
|
75
|
-
export type ApiSingleOrGroupedAreaData = Flatten<Exclude<ApiSingleOrGroupedArea, ApiError>['data']>;
|
|
76
|
-
|
|
77
|
-
export const singleOrGroupedAreaRevivers: Revivers<ApiSingleOrGroupedArea, SingleOrGroupedArea> = {
|
|
78
|
-
...boundingBoxRevivers,
|
|
79
|
-
properties: (data: ApiSingleOrGroupedAreaData) =>
|
|
80
|
-
(data.properties != null ? JSON.parse(data.properties) as Record<string, string> : null),
|
|
81
|
-
};
|
|
56
|
+
export type SingleOrGroupedArea = SingleArea | GroupedArea;
|
|
82
57
|
|
|
83
|
-
/**
|
|
84
|
-
* TODO When SAGA search on click is implemented, remove mode and make searchBounds required
|
|
85
|
-
*/
|
|
86
58
|
export async function searchSingleOrGroupedAreas(
|
|
87
59
|
language: string,
|
|
88
60
|
search: RequireAtLeastOne<{
|
|
@@ -92,22 +64,29 @@ export async function searchSingleOrGroupedAreas(
|
|
|
92
64
|
}>,
|
|
93
65
|
mode: 'polygon' | 'group' | 'both' = 'both',
|
|
94
66
|
): Promise<SingleOrGroupedArea[]> {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
67
|
+
/**
|
|
68
|
+
* TODO When SAGA search on click is implemented, remove mode and make searchBounds required
|
|
69
|
+
*/
|
|
70
|
+
return request<ApiSingleOrGroupedAreaArray, SingleOrGroupedArea>(
|
|
71
|
+
`/v1/choropleth/polygons/search?${getSearchParams({
|
|
72
|
+
language,
|
|
73
|
+
...search.searchBounds,
|
|
74
|
+
...(search.query && { query: search.query }),
|
|
75
|
+
...(search.searchPoint && { point: search.searchPoint }),
|
|
76
|
+
mode,
|
|
77
|
+
})}`,
|
|
78
|
+
).then(result => {
|
|
79
|
+
result.forEach(
|
|
80
|
+
elem => {
|
|
81
|
+
elem.boundingBox = JSON.parse(elem.boundingBox as unknown as string) as Polygon;
|
|
82
|
+
if (!elem.isGroup) {
|
|
83
|
+
elem.properties = JSON.parse(elem.properties as unknown as string) as Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return result;
|
|
102
89
|
});
|
|
103
|
-
const path = `${pathname}?${query}`;
|
|
104
|
-
const options = { revivers: singleOrGroupedAreaRevivers };
|
|
105
|
-
|
|
106
|
-
type ApiSingleOrGroupedAreaArray = {
|
|
107
|
-
data: ApiSingleOrGroupedAreaData[];
|
|
108
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
109
|
-
|
|
110
|
-
return request<ApiSingleOrGroupedAreaArray, SingleOrGroupedArea>(path, null, null, options);
|
|
111
90
|
}
|
|
112
91
|
|
|
113
92
|
export type GroupedAreaChild = {
|
|
@@ -135,7 +114,7 @@ type ApiGroupedAreaChild = {
|
|
|
135
114
|
export type ApiGroupedAreaChildData = Flatten<Exclude<ApiGroupedAreaChild, ApiError>['data']>;
|
|
136
115
|
|
|
137
116
|
export const groupedAreaChildRevivers: Revivers<ApiGroupedAreaChild, GroupedAreaChild> = {
|
|
138
|
-
|
|
117
|
+
boundingBox: (data: ApiGroupedAreaChildData) => JSON.parse(data.bounding_box) as Polygon,
|
|
139
118
|
properties: (data: ApiGroupedAreaChildData) => JSON.parse(data.properties) as Record<string, string>,
|
|
140
119
|
};
|
|
141
120
|
|
|
@@ -145,84 +124,9 @@ export async function groupedAreaChildren(groupId: number, language: string): Pr
|
|
|
145
124
|
const path = `${pathname}?${query}`;
|
|
146
125
|
const options = { revivers: groupedAreaChildRevivers };
|
|
147
126
|
|
|
148
|
-
type
|
|
127
|
+
type ApiGroupedAreaChildArray = {
|
|
149
128
|
data: ApiGroupedAreaChildData[];
|
|
150
129
|
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
151
130
|
|
|
152
|
-
return request<
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export type MatchedGroup = {
|
|
156
|
-
id: number;
|
|
157
|
-
sml: number;
|
|
158
|
-
childrenCount: number;
|
|
159
|
-
matchField: string;
|
|
160
|
-
boundingBox: Polygon;
|
|
161
|
-
property: string;
|
|
162
|
-
name: string;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
export type ApiMatchedGroup = {
|
|
166
|
-
data: {
|
|
167
|
-
id: number;
|
|
168
|
-
sml: number;
|
|
169
|
-
children_count: number;
|
|
170
|
-
match_field: string;
|
|
171
|
-
bounding_box: string;
|
|
172
|
-
property: string;
|
|
173
|
-
name: string;
|
|
174
|
-
};
|
|
175
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
176
|
-
|
|
177
|
-
export type ApiMatchedGroupData = Flatten<Exclude<ApiMatchedGroup, ApiError>['data']>;
|
|
178
|
-
|
|
179
|
-
export async function getGroupsByDataSample(
|
|
180
|
-
sample: Record<string, string[]>,
|
|
181
|
-
language: string,
|
|
182
|
-
rowCount: number,
|
|
183
|
-
): Promise<MatchedGroup[]> {
|
|
184
|
-
const path = `/v1/choropleth/groups/sample`;
|
|
185
|
-
const options = { revivers: boundingBoxRevivers };
|
|
186
|
-
|
|
187
|
-
type ApiMatchedGroupArray = {
|
|
188
|
-
data: ApiMatchedGroupData[];
|
|
189
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
190
|
-
|
|
191
|
-
return request<ApiMatchedGroupArray, MatchedGroup>(path, { sample, language, row_count: rowCount }, null, options);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export type BoundPolygon = {
|
|
195
|
-
index: number;
|
|
196
|
-
inputName: string;
|
|
197
|
-
id: number;
|
|
198
|
-
sml: number;
|
|
199
|
-
name: string;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export type ApiBoundPolygon = {
|
|
203
|
-
data: {
|
|
204
|
-
index: number;
|
|
205
|
-
input_name: string;
|
|
206
|
-
id: number;
|
|
207
|
-
sml: number;
|
|
208
|
-
name: string;
|
|
209
|
-
};
|
|
210
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
211
|
-
|
|
212
|
-
export type ApiBoundPolygonData = Flatten<Exclude<ApiBoundPolygon, ApiError>['data']>;
|
|
213
|
-
|
|
214
|
-
export async function getBoundPolygons(
|
|
215
|
-
groupId: number,
|
|
216
|
-
property: string,
|
|
217
|
-
data: string[],
|
|
218
|
-
language: string,
|
|
219
|
-
): Promise<BoundPolygon[]> {
|
|
220
|
-
const path = `/v1/choropleth/groups/bind`;
|
|
221
|
-
const body = { group_id: groupId, property, data, language };
|
|
222
|
-
|
|
223
|
-
type ApiBoundPolygonArray = {
|
|
224
|
-
data: ApiBoundPolygonData[];
|
|
225
|
-
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
226
|
-
|
|
227
|
-
return request<ApiBoundPolygonArray, BoundPolygon>(path, body);
|
|
131
|
+
return request<ApiGroupedAreaChildArray, GroupedAreaChild>(path, null, null, options);
|
|
228
132
|
}
|
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/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 {
|
|
@@ -221,10 +221,9 @@ export async function getJobRevisionOutput(jobId: number): Promise<JobRevisionOu
|
|
|
221
221
|
await createJobRevisionBuild(jobId);
|
|
222
222
|
|
|
223
223
|
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
|
-
});
|
|
224
|
+
const headers = getAuthorizationHeaders('GET');
|
|
225
|
+
const response = await fetch(href, { headers, ...!token && { credentials: 'include' } })
|
|
226
|
+
.catch((error: Error) => { throw new NetworkError(error?.message ?? error) });
|
|
228
227
|
|
|
229
228
|
if (response.ok) {
|
|
230
229
|
const blob = await response.blob().catch(() => {
|
package/src/api/mapstyleSet.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type ApiCommonData,
|
|
3
|
+
type ApiError,
|
|
4
|
+
type ApiSuccess,
|
|
5
|
+
type Flatten,
|
|
6
|
+
type Revivers,
|
|
7
|
+
defaultListHeader,
|
|
8
|
+
getSearchParams,
|
|
9
|
+
request,
|
|
10
|
+
} from '../utils.js';
|
|
2
11
|
|
|
3
12
|
export type MapstyleSet = {
|
|
4
13
|
id: number;
|
|
@@ -46,3 +55,16 @@ export const mapstyleSetRevivers: Revivers<ApiMapstyleSet, MapstyleSet> = {
|
|
|
46
55
|
order: undefined,
|
|
47
56
|
custom_settings: undefined,
|
|
48
57
|
};
|
|
58
|
+
|
|
59
|
+
export async function listMapstyleSets(jobTypeId?: number): Promise<MapstyleSet[]> {
|
|
60
|
+
const pathname = `/v1/mapstyles/sets`;
|
|
61
|
+
const query = jobTypeId !== undefined ? getSearchParams({ search: { job_type_id: jobTypeId } }) : null;
|
|
62
|
+
const path = query ? `${pathname}?${query}` : pathname;
|
|
63
|
+
const options = { revivers: mapstyleSetRevivers };
|
|
64
|
+
|
|
65
|
+
type ApiMapstyleSetArray = {
|
|
66
|
+
data: ApiMapstyleSetData[];
|
|
67
|
+
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
68
|
+
|
|
69
|
+
return request<ApiMapstyleSetArray, MapstyleSet>(path, null, defaultListHeader, options);
|
|
70
|
+
}
|
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
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { apiHost, authenticate, getAuthorizationHeaders, token } from './oauth.js';
|
|
1
2
|
import type { CamelCasedProperties, SnakeCasedProperties } from 'type-fest';
|
|
2
|
-
import { apiHost, authenticate, token } from './oauth.js';
|
|
3
3
|
|
|
4
4
|
export type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
|
|
5
5
|
|
|
@@ -233,7 +233,6 @@ function getRequestInit<I extends ApiCommon, O extends Record<string, unknown>>(
|
|
|
233
233
|
extraHeaders?: Record<string, string> | null,
|
|
234
234
|
extraOptions?: ExtraOptions<I, O>,
|
|
235
235
|
): RequestInit {
|
|
236
|
-
const authorization = token ? { Authorization: token.toString() } : null;
|
|
237
236
|
let contentType = null as { 'Content-Type': string } | null;
|
|
238
237
|
|
|
239
238
|
if (body !== undefined) {
|
|
@@ -259,10 +258,11 @@ function getRequestInit<I extends ApiCommon, O extends Record<string, unknown>>(
|
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
260
|
|
|
262
|
-
const headers = { Accept: 'application/json', ...authorization, ...contentType, ...extraHeaders };
|
|
263
261
|
const method = extraOptions?.method ?? (body != null ? 'POST' : 'GET'); // don't touch `!=` please
|
|
262
|
+
const authorization = getAuthorizationHeaders(method);
|
|
263
|
+
const headers = { Accept: 'application/json', ...authorization, ...contentType, ...extraHeaders };
|
|
264
264
|
|
|
265
|
-
return { body, headers, method } as RequestInit;
|
|
265
|
+
return { body, headers, method, ...!token && { credentials: 'include' } } as RequestInit;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
interface Context<I extends ApiCommon, O extends Record<string, unknown>> {
|