@sjolystinnovation/app-kit 0.16.0 → 0.17.0
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/README.md +25 -0
- package/package.json +1 -1
- package/src/api/index.ts +9 -0
- package/src/api/services.ts +197 -0
package/README.md
CHANGED
|
@@ -264,6 +264,31 @@ export const getRecipe = (id: number) =>
|
|
|
264
264
|
`target`, so an edit shows at once. It posts to `/api/revalidate` unless `endpoint` says otherwise.
|
|
265
265
|
It never throws. On failure the pages' own revalidate window is the fallback.
|
|
266
266
|
|
|
267
|
+
`createPublicClient(baseURL)` is the same without the session, for the API's anonymous endpoints. An
|
|
268
|
+
empty `baseURL` throws, and requests only go to `baseURL`.
|
|
269
|
+
|
|
270
|
+
The services below call the endpoints the SjolystInnovation.AppKit .NET packages map, so an app does
|
|
271
|
+
not write them. `api` is a `createApiClient` instance and `publicApi` a `createPublicClient` one.
|
|
272
|
+
Failures throw an `Error` with the API's message, or with the service's own English fallback.
|
|
273
|
+
`onSaved` runs after a save succeeds, such as `() => requestRevalidate("branding")`.
|
|
274
|
+
|
|
275
|
+
| Factory | Methods | .NET package |
|
|
276
|
+
| --- | --- | --- |
|
|
277
|
+
| `createUserService(api, publicApi)` | `requestPasswordReset`, `resetPassword`, and for the signed-in user `getProfile`, `updateProfile`, `changePassword`, `exportData`, `deleteAccount` | Auth |
|
|
278
|
+
| `createAdminUsersService(api)` | `getUsers(includeDeleted?)`, `addRole`, `removeRole`, `deleteUser`, `invite` | Auth |
|
|
279
|
+
| `createSettingsService(api, { onSaved? })` | `get`, `update` | SiteSettings |
|
|
280
|
+
| `createBrandingService(api, publicApi, { onSaved? })` | `get`, which answers `{}` when the API cannot be read, and `update(form)` | SiteSettings |
|
|
281
|
+
| `createLegalService<Key>(api, publicApi, { onSaved? })` | `get(key, locale)`, which answers `null` for a missing page, `listAll`, `save` | SiteSettings |
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { createApiClient, createPublicClient, createUserService } from "@sjolystinnovation/app-kit/api";
|
|
285
|
+
|
|
286
|
+
const apiUrl = requireEnv("NEXT_PUBLIC_API_URL", process.env.NEXT_PUBLIC_API_URL);
|
|
287
|
+
export default createUserService(createApiClient(apiUrl), createPublicClient(apiUrl));
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The response types are exported too: `UserProfile`, `AdminUser`, `SiteSettings` and `LegalPage`.
|
|
291
|
+
|
|
267
292
|
### `@sjolystinnovation/app-kit/server`
|
|
268
293
|
|
|
269
294
|
For server components and route handlers only. It imports `next/cache` and `next/server`.
|
package/package.json
CHANGED
package/src/api/index.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export { createApiClient } from "./createApiClient";
|
|
2
2
|
export { request } from "./request";
|
|
3
3
|
export { requestRevalidate } from "./requestRevalidate";
|
|
4
|
+
export {
|
|
5
|
+
createPublicClient,
|
|
6
|
+
createUserService,
|
|
7
|
+
createSettingsService,
|
|
8
|
+
createBrandingService,
|
|
9
|
+
createLegalService,
|
|
10
|
+
createAdminUsersService,
|
|
11
|
+
} from "./services";
|
|
12
|
+
export type { OnSaved, UserProfile, SiteSettings, LegalPage, AdminUser } from "./services";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import axios, { type AxiosInstance } from "axios";
|
|
2
|
+
import { getSession } from "next-auth/react";
|
|
3
|
+
import type { Branding } from "../branding/types";
|
|
4
|
+
import { request } from "./request";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An axios instance for the API's anonymous endpoints, without the session's token. Like
|
|
8
|
+
* `createApiClient`, it only sends requests to `baseURL`.
|
|
9
|
+
*/
|
|
10
|
+
export function createPublicClient(baseURL: string): AxiosInstance {
|
|
11
|
+
if (!baseURL) {
|
|
12
|
+
throw new Error("createPublicClient needs the API base URL, and baseURL is empty.");
|
|
13
|
+
}
|
|
14
|
+
return axios.create({ baseURL, allowAbsoluteUrls: false });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Runs after an admin change saves, such as to purge the pages that show it. */
|
|
18
|
+
export type OnSaved = () => Promise<void> | void;
|
|
19
|
+
|
|
20
|
+
async function currentUserId(): Promise<string> {
|
|
21
|
+
const session = await getSession();
|
|
22
|
+
const id = (session?.user as { id?: string } | undefined)?.id;
|
|
23
|
+
if (!id) throw new Error("Not signed in.");
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The signed-in user's account, as SjolystInnovation.AppKit.Auth's account endpoints return it. */
|
|
28
|
+
export interface UserProfile {
|
|
29
|
+
id: string;
|
|
30
|
+
userName: string;
|
|
31
|
+
firstName: string;
|
|
32
|
+
lastName: string;
|
|
33
|
+
email: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Password reset and the signed-in user's account, on the auth and account endpoints of
|
|
39
|
+
* SjolystInnovation.AppKit.Auth. `api` carries the session, `publicApi` does not.
|
|
40
|
+
*/
|
|
41
|
+
export function createUserService(api: AxiosInstance, publicApi: AxiosInstance) {
|
|
42
|
+
return {
|
|
43
|
+
requestPasswordReset: (data: { email: string }) =>
|
|
44
|
+
request(() => publicApi.post<{ message: string }>("/auth/request-password-reset", data), "Failed to request password reset code"),
|
|
45
|
+
|
|
46
|
+
resetPassword: (data: { email: string; code: string; newPassword: string }) =>
|
|
47
|
+
request(() => publicApi.post<{ message: string }>("/auth/reset-password", data), "Failed to reset password"),
|
|
48
|
+
|
|
49
|
+
async changePassword(data: { currentPassword: string; newPassword: string }) {
|
|
50
|
+
const id = await currentUserId();
|
|
51
|
+
return request(() => api.put<{ message: string }>(`/users/${encodeURIComponent(id)}/password`, data), "Failed to change password");
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async getProfile() {
|
|
55
|
+
const id = await currentUserId();
|
|
56
|
+
return request(() => api.get<UserProfile>(`/users/${encodeURIComponent(id)}/profile`), "Failed to load profile");
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async updateProfile(data: { firstName: string; lastName: string }) {
|
|
60
|
+
const id = await currentUserId();
|
|
61
|
+
return request(() => api.put<UserProfile>(`/users/${encodeURIComponent(id)}/profile`, data), "Failed to update profile");
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async exportData() {
|
|
65
|
+
const id = await currentUserId();
|
|
66
|
+
return request(() => api.get<unknown>(`/users/${encodeURIComponent(id)}/export`), "Failed to export data");
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async deleteAccount() {
|
|
70
|
+
const id = await currentUserId();
|
|
71
|
+
await request(() => api.delete(`/users/${encodeURIComponent(id)}/account`), "Failed to delete account");
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The company settings, as SjolystInnovation.AppKit.SiteSettings's `/settings` endpoint holds them. */
|
|
77
|
+
export interface SiteSettings {
|
|
78
|
+
contactRecipientEmail: string;
|
|
79
|
+
companyName: string;
|
|
80
|
+
companyLegalName: string;
|
|
81
|
+
orgNumber: string;
|
|
82
|
+
vatRegistered: boolean;
|
|
83
|
+
streetAddress: string;
|
|
84
|
+
postalCode: string;
|
|
85
|
+
addressLocality: string;
|
|
86
|
+
addressRegion: string;
|
|
87
|
+
publicEmail: string;
|
|
88
|
+
publicPhone: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The admin's company settings on SjolystInnovation.AppKit.SiteSettings. */
|
|
92
|
+
export function createSettingsService(api: AxiosInstance, options: { onSaved?: OnSaved } = {}) {
|
|
93
|
+
return {
|
|
94
|
+
get: () => request(() => api.get<SiteSettings>("/settings"), "Failed to load settings"),
|
|
95
|
+
|
|
96
|
+
async update(data: SiteSettings) {
|
|
97
|
+
const saved = await request(() => api.put<SiteSettings>("/settings", data), "Failed to update settings");
|
|
98
|
+
await options.onSaved?.();
|
|
99
|
+
return saved;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The logo and favicon on SjolystInnovation.AppKit.SiteSettings. Reading is anonymous and answers
|
|
106
|
+
* no branding when the API cannot be reached, so a page still renders.
|
|
107
|
+
*/
|
|
108
|
+
export function createBrandingService(api: AxiosInstance, publicApi: AxiosInstance, options: { onSaved?: OnSaved } = {}) {
|
|
109
|
+
return {
|
|
110
|
+
async get(): Promise<Branding> {
|
|
111
|
+
try {
|
|
112
|
+
return (await publicApi.get<Branding>("/branding")).data;
|
|
113
|
+
} catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/** Sends the form `BrandingManager` builds: `logo`, `icon`, `removeLogo` and `removeIcon`. */
|
|
119
|
+
async update(form: FormData): Promise<Branding> {
|
|
120
|
+
const saved = await request(() => api.put<Branding>("/branding", form), "Failed to save branding");
|
|
121
|
+
await options.onSaved?.();
|
|
122
|
+
return saved;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A legal page, as SjolystInnovation.AppKit.SiteSettings's legal endpoints return it. */
|
|
128
|
+
export interface LegalPage<Key extends string = string> {
|
|
129
|
+
key: Key;
|
|
130
|
+
locale: string;
|
|
131
|
+
title: string;
|
|
132
|
+
bodyMarkdown: string;
|
|
133
|
+
updatedAt: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The terms, privacy and cookie pages on SjolystInnovation.AppKit.SiteSettings. Reading is anonymous
|
|
138
|
+
* and answers null for a page that is missing or cannot be read, so a page shows its fallback.
|
|
139
|
+
*/
|
|
140
|
+
export function createLegalService<Key extends string = "terms" | "privacy" | "cookies">(
|
|
141
|
+
api: AxiosInstance,
|
|
142
|
+
publicApi: AxiosInstance,
|
|
143
|
+
options: { onSaved?: OnSaved } = {},
|
|
144
|
+
) {
|
|
145
|
+
return {
|
|
146
|
+
async get(key: Key, locale: string): Promise<LegalPage<Key> | null> {
|
|
147
|
+
try {
|
|
148
|
+
return (await publicApi.get<LegalPage<Key>>(`/content/legal/${encodeURIComponent(key)}`, { params: { locale } })).data;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
listAll: () => request(() => api.get<LegalPage<Key>[]>("/content/legal"), "Failed to load legal pages"),
|
|
155
|
+
|
|
156
|
+
async save(key: Key, locale: string, title: string, bodyMarkdown: string) {
|
|
157
|
+
const page = await request(
|
|
158
|
+
() => api.put<LegalPage<Key>>(`/content/legal/${encodeURIComponent(key)}/${encodeURIComponent(locale)}`, { title, bodyMarkdown }),
|
|
159
|
+
"Failed to save legal page",
|
|
160
|
+
);
|
|
161
|
+
await options.onSaved?.();
|
|
162
|
+
return page;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A user in the admin list, as SjolystInnovation.AppKit.Auth's user admin endpoints return it. */
|
|
168
|
+
export interface AdminUser {
|
|
169
|
+
id: string;
|
|
170
|
+
userName: string;
|
|
171
|
+
email: string;
|
|
172
|
+
firstName: string;
|
|
173
|
+
lastName: string;
|
|
174
|
+
createdAt: string;
|
|
175
|
+
isDeleted: boolean;
|
|
176
|
+
roles: string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** User administration on SjolystInnovation.AppKit.Auth: the list, roles, invites and deletion. */
|
|
180
|
+
export function createAdminUsersService(api: AxiosInstance) {
|
|
181
|
+
return {
|
|
182
|
+
getUsers: (includeDeleted = false) =>
|
|
183
|
+
request(() => api.get<AdminUser[]>("/users", { params: { includeDeleted } }), "Failed to load users"),
|
|
184
|
+
|
|
185
|
+
addRole: (userId: string, role: string) =>
|
|
186
|
+
request(() => api.post(`/users/${encodeURIComponent(userId)}/roles`, { role }), "Failed to assign role"),
|
|
187
|
+
|
|
188
|
+
removeRole: (userId: string, role: string) =>
|
|
189
|
+
request(() => api.delete(`/users/${encodeURIComponent(userId)}/roles/${encodeURIComponent(role)}`), "Failed to remove role"),
|
|
190
|
+
|
|
191
|
+
deleteUser: (userId: string) =>
|
|
192
|
+
request(() => api.delete(`/users/${encodeURIComponent(userId)}/account`), "Failed to delete user"),
|
|
193
|
+
|
|
194
|
+
invite: (email: string, firstName: string, lastName: string, role: string) =>
|
|
195
|
+
request(() => api.post("/users/invite", { email, firstName, lastName, role }), "Failed to send invitation"),
|
|
196
|
+
};
|
|
197
|
+
}
|