@omerlo/omerlo-webkit 0.0.2
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 +208 -0
- package/dist/components/OmerloWebkit.svelte +26 -0
- package/dist/components/OmerloWebkit.svelte.d.ts +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/omerlo/index.d.ts +13 -0
- package/dist/omerlo/index.js +2 -0
- package/dist/omerlo/reader/endpoints/accounts.d.ts +19 -0
- package/dist/omerlo/reader/endpoints/accounts.js +42 -0
- package/dist/omerlo/reader/endpoints/device.d.ts +8 -0
- package/dist/omerlo/reader/endpoints/device.js +13 -0
- package/dist/omerlo/reader/endpoints/notification.d.ts +23 -0
- package/dist/omerlo/reader/endpoints/notification.js +40 -0
- package/dist/omerlo/reader/endpoints/oauth.d.ts +23 -0
- package/dist/omerlo/reader/endpoints/oauth.js +38 -0
- package/dist/omerlo/reader/fetchers.d.ts +13 -0
- package/dist/omerlo/reader/fetchers.js +12 -0
- package/dist/omerlo/reader/index.d.ts +3 -0
- package/dist/omerlo/reader/index.js +1 -0
- package/dist/omerlo/reader/server/hooks.d.ts +3 -0
- package/dist/omerlo/reader/server/hooks.js +73 -0
- package/dist/omerlo/reader/server/index.d.ts +4 -0
- package/dist/omerlo/reader/server/index.js +5 -0
- package/dist/omerlo/reader/server/token.d.ts +23 -0
- package/dist/omerlo/reader/server/token.js +63 -0
- package/dist/omerlo/reader/server/utils.d.ts +14 -0
- package/dist/omerlo/reader/server/utils.js +82 -0
- package/dist/omerlo/reader/stores/user_session.d.ts +14 -0
- package/dist/omerlo/reader/stores/user_session.js +58 -0
- package/dist/omerlo/reader/utils/api.d.ts +29 -0
- package/dist/omerlo/reader/utils/api.js +27 -0
- package/dist/omerlo/reader/utils/assocs.d.ts +15 -0
- package/dist/omerlo/reader/utils/assocs.js +32 -0
- package/dist/omerlo/reader/utils/request.d.ts +15 -0
- package/dist/omerlo/reader/utils/request.js +27 -0
- package/dist/omerlo/reader/utils/response.d.ts +6 -0
- package/dist/omerlo/reader/utils/response.js +6 -0
- package/dist/omerlo/server.d.ts +1 -0
- package/dist/omerlo/server.js +1 -0
- package/eslint.config.js +34 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Omerlo WebKit
|
|
2
|
+
|
|
3
|
+
This webkit is a wapper arround Omerlo's API to create quickly a website using Omerlo.
|
|
4
|
+
|
|
5
|
+
The user's session and tokens will be managed by Omerlo Webkit. The connection state is dispatched
|
|
6
|
+
over all window's tab, no need to refresh other tabs.
|
|
7
|
+
|
|
8
|
+
## Using the omerlo webkit
|
|
9
|
+
|
|
10
|
+
###
|
|
11
|
+
|
|
12
|
+
Install the package `omerlo-webkit`
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i omerlo-webkit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Create the .env
|
|
19
|
+
|
|
20
|
+
Copy the `.env.dist` to `.env` then update its values to match your Omerlo's application informations.
|
|
21
|
+
|
|
22
|
+
### Create the server's hooks
|
|
23
|
+
|
|
24
|
+
Create (or update) the file `src/hooks.server.ts` and add required hooks as follow
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import type { Handle } from "@sveltejs/kit";
|
|
28
|
+
|
|
29
|
+
import { handleUserToken, handleReaderApi } from 'omerlo-webkit/reader/server';
|
|
30
|
+
import { sequence } from "@sveltejs/kit/hooks";
|
|
31
|
+
|
|
32
|
+
// You can add custom hooks too
|
|
33
|
+
const handleLocale: Handle = async ({ event, resolve }) => {
|
|
34
|
+
const userLocale = 'en'; // you can fetch this value from cookie or w.e
|
|
35
|
+
event.url.searchParams.append('locale', userLocale);
|
|
36
|
+
return resolve(event);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const handle = sequence(handleLocale, handleUserToken, handleReaderApi);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Layout `layout.server.ts`
|
|
43
|
+
|
|
44
|
+
To automatically load the user session, you have to load the user's session as following
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// +layout.server.ts
|
|
48
|
+
import type { LayoutServerLoad } from './$types';
|
|
49
|
+
|
|
50
|
+
import { loadUserSession } from 'omerlo-webkit/reader/server';
|
|
51
|
+
import { type UserSession } from 'omerlo-webkit/reader';
|
|
52
|
+
|
|
53
|
+
export const load: LayoutServerLoad = async ({ fetch, cookies }) => {
|
|
54
|
+
const userSession: UserSession = await loadUserSession(fetch, cookies);
|
|
55
|
+
|
|
56
|
+
return { userSession };
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then you need to use the component `OmerloWebkit` and give him the `userSession` previously loaded.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// +layout.svelte
|
|
64
|
+
<script lang="ts">
|
|
65
|
+
import { OmerloWebkit } from 'omerlo-webkit'
|
|
66
|
+
let { data, children } = $props();
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<OmerloWebkit userSession={data.userSession}>
|
|
70
|
+
{@render children?.()}
|
|
71
|
+
</OmerloWebkit>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Use Reader API
|
|
75
|
+
|
|
76
|
+
To use reader API, you can use the function `useReader` passing the sveltekit `fetch` function.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// +page.ts
|
|
80
|
+
import type { PageLoad } from './$types';
|
|
81
|
+
import { useReader } from 'omerlo-webkit';
|
|
82
|
+
|
|
83
|
+
export const load: PageLoad = async ({ fetch }) => {
|
|
84
|
+
const oauthProviders = await useReader(fetch).listOauthProviders();
|
|
85
|
+
return { oauthProviders };
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Connecting user using Oauth Provider (such as Google).
|
|
90
|
+
|
|
91
|
+
You'll need to create a login endpoint to add some required params such as the `state`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// +server.ts
|
|
95
|
+
import { error, redirect, type RequestHandler } from "@sveltejs/kit";
|
|
96
|
+
import { env } from '$env/dynamic/private';
|
|
97
|
+
import jwt from 'jsonwebtoken';
|
|
98
|
+
|
|
99
|
+
export const GET: RequestHandler = ({ url }) => {
|
|
100
|
+
const oauthUrl = url.searchParams.get('oauthUrl');
|
|
101
|
+
const oauthProviderId = url.searchParams.get('oauthProviderId');
|
|
102
|
+
|
|
103
|
+
if (!oauthUrl) error(400, 'Missing oauthUrl query parameter');
|
|
104
|
+
if (!oauthProviderId) error(400, 'Missing oauthProviderId query parameter');
|
|
105
|
+
|
|
106
|
+
const currentPath = url.searchParams.get('currentPath') || '/';
|
|
107
|
+
const state = jwt.sign({ currentPath, oauthProviderId }, env.PRIVATE_JWT_SECRET, { expiresIn: '1h' })
|
|
108
|
+
|
|
109
|
+
const redirectUrl = new URL(oauthUrl);
|
|
110
|
+
redirectUrl.searchParams.set('state', state);
|
|
111
|
+
redirectUrl.searchParams.set('redirect_uri', url.origin +'/oauth/callback');
|
|
112
|
+
|
|
113
|
+
redirect(302, redirectUrl);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
And then create a callback action.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { error, redirect, type RequestHandler } from "@sveltejs/kit";
|
|
121
|
+
import { env } from '$env/dynamic/private';
|
|
122
|
+
import jwt from 'jsonwebtoken';
|
|
123
|
+
import { exchangeAuthorizationCode, setAuthorizationCookies } from "omerlo-webkit/reader/server";
|
|
124
|
+
|
|
125
|
+
export const GET: RequestHandler = async ({ url, cookies }) => {
|
|
126
|
+
const redirectUri = url.origin + url.pathname;
|
|
127
|
+
const state = getRequiredQueryParams(url, 'state');
|
|
128
|
+
const code = getRequiredQueryParams(url, 'code');
|
|
129
|
+
const {oauthProviderId, currentPath} = parseJwt(state);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const token = await exchangeAuthorizationCode({code, redirectUri, oauthProviderId})
|
|
133
|
+
setAuthorizationCookies(cookies, token);
|
|
134
|
+
} catch(_err) {
|
|
135
|
+
error(401, "Could not authenticate from the provider");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
redirect(303, currentPath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getRequiredQueryParams(url: URL, paramsName: string): string {
|
|
142
|
+
const value = url.searchParams.get(paramsName);
|
|
143
|
+
|
|
144
|
+
if (!value) {
|
|
145
|
+
error(400, `Missing ${paramsName}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface State {
|
|
152
|
+
oauthProviderId: string,
|
|
153
|
+
currentPath: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseJwt(state: string): State {
|
|
157
|
+
try {
|
|
158
|
+
return jwt.verify(state, env.PRIVATE_JWT_SECRET) as State;
|
|
159
|
+
} catch(_err) {
|
|
160
|
+
error(400, 'Invalid state');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Logout your user
|
|
166
|
+
|
|
167
|
+
To logout your user you can create an endpoint that will drop cookies used for authentication.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// +server.ts
|
|
171
|
+
import { json, type RequestHandler } from "@sveltejs/kit";
|
|
172
|
+
import { clearAuthorizationCookies } from 'omerlo-webkit/reader/server';
|
|
173
|
+
|
|
174
|
+
export const DELETE: RequestHandler = ({ cookies }) => {
|
|
175
|
+
clearAuthorizationCookies(cookies);
|
|
176
|
+
return json(201);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
When a user is logout for any reason (401 or manually logout), the `invalidate('omerlo:user_session)` is triggered,
|
|
181
|
+
allowing you to reload component / page that have `depends('omerlo:user_session)`.
|
|
182
|
+
|
|
183
|
+
Plus the user's session store is updated cross tabs. It mean the user will be logout from every tabs.
|
|
184
|
+
|
|
185
|
+
## Use user's informations
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
<script lang="ts">
|
|
189
|
+
import { getUserSession } from 'omerlo-webkit/reader';
|
|
190
|
+
|
|
191
|
+
let { data } = $props();
|
|
192
|
+
const userSession = getUserSession();
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<!-- Some token may not be linked to a reader accounts -->
|
|
196
|
+
<!-- In that case, the user will be null -->
|
|
197
|
+
{#if $userSession.user}
|
|
198
|
+
Your name: {$userSession.user?.name}
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
{#if $userSession.verified}
|
|
202
|
+
<div> You has been verified </div>
|
|
203
|
+
{/if}
|
|
204
|
+
|
|
205
|
+
{#if $userSession.authenticated}
|
|
206
|
+
<div> You has been authenticated </div>
|
|
207
|
+
{/if}
|
|
208
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { initUserSession, type UserSession } from "../omerlo/reader/stores/user_session";
|
|
3
|
+
import { onDestroy, onMount } from "svelte";
|
|
4
|
+
|
|
5
|
+
export let userSession: UserSession;
|
|
6
|
+
let selfComponent: HTMLDivElement;
|
|
7
|
+
|
|
8
|
+
const _userSession = initUserSession(userSession);
|
|
9
|
+
|
|
10
|
+
onMount(() => {
|
|
11
|
+
selfComponent.addEventListener('logout', () => {
|
|
12
|
+
_userSession.handleLogout();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
onDestroy(() => {
|
|
17
|
+
if (selfComponent) {
|
|
18
|
+
selfComponent.removeEventListener('logout', _userSession.handleLogout, true);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div id="omerlo-webkit" bind:this={selfComponent}>
|
|
24
|
+
<slot></slot>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type UserSession } from "../omerlo/reader/stores/user_session";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: Props & {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
|
|
16
|
+
default: any;
|
|
17
|
+
} ? Props extends Record<string, never> ? any : {
|
|
18
|
+
children?: any;
|
|
19
|
+
} : {});
|
|
20
|
+
declare const OmerloWebkit: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{
|
|
21
|
+
userSession: UserSession;
|
|
22
|
+
}, {
|
|
23
|
+
default: {};
|
|
24
|
+
}>, {
|
|
25
|
+
[evt: string]: CustomEvent<any>;
|
|
26
|
+
}, {
|
|
27
|
+
default: {};
|
|
28
|
+
}, {}, string>;
|
|
29
|
+
type OmerloWebkit = InstanceType<typeof OmerloWebkit>;
|
|
30
|
+
export default OmerloWebkit;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const useReader: (f: typeof fetch) => {
|
|
2
|
+
notifications: {
|
|
3
|
+
listTopics: (params?: Partial<import("./reader/utils/api").PagingParams>) => Promise<import("./reader/utils/api").ApiResponse<import("./reader/endpoints/notification").TopicSummary[]>>;
|
|
4
|
+
subscribeToTopic: (params: import("./reader/endpoints/notification").SubscribtionParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
5
|
+
unsubscribeFromTopic: (params: import("./reader/endpoints/notification").SubscribtionParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
6
|
+
};
|
|
7
|
+
listOauthProviders: (params?: Partial<import("./reader/utils/api").PagingParams>) => Promise<import("./reader/utils/api").ApiResponse<import("./reader").OauthProviderSummary[]>>;
|
|
8
|
+
getOauthUser: () => Promise<import("./reader/utils/api").ApiResponse<import("./reader").OauthUser>>;
|
|
9
|
+
registerDevice: (params: import("./reader/endpoints/device").DeviceParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
10
|
+
userInfo: () => Promise<import("./reader/utils/api").ApiResponse<import("./reader").UserInfo>>;
|
|
11
|
+
verifyAccount: (params: import("./reader").VerifyAccountParams) => void;
|
|
12
|
+
validateAccount: (params: import("./reader").ValidateAccountParams) => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const accountsFetchers: (f: typeof fetch) => {
|
|
2
|
+
userInfo: () => Promise<import("../utils/api").ApiResponse<UserInfo>>;
|
|
3
|
+
verifyAccount: (params: VerifyAccountParams) => void;
|
|
4
|
+
validateAccount: (params: ValidateAccountParams) => void;
|
|
5
|
+
};
|
|
6
|
+
export interface ValidateAccountParams {
|
|
7
|
+
email: string;
|
|
8
|
+
callbackUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateAccount(f: typeof fetch): (params: ValidateAccountParams) => void;
|
|
11
|
+
export interface VerifyAccountParams {
|
|
12
|
+
verification_token: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function verifyAccount(f: typeof fetch): (params: VerifyAccountParams) => void;
|
|
15
|
+
export declare function getUserInfo(f: typeof fetch): () => Promise<import("../utils/api").ApiResponse<UserInfo>>;
|
|
16
|
+
export interface UserInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { request } from '../utils/request';
|
|
2
|
+
export const accountsFetchers = (f) => {
|
|
3
|
+
return {
|
|
4
|
+
userInfo: getUserInfo(f),
|
|
5
|
+
verifyAccount: verifyAccount(f),
|
|
6
|
+
validateAccount: validateAccount(f)
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
//
|
|
10
|
+
// Validate an account using the bearer token.
|
|
11
|
+
//
|
|
12
|
+
export function validateAccount(f) {
|
|
13
|
+
return (params) => {
|
|
14
|
+
const queryParams = { email: params.email, callback_url: params.callbackUrl };
|
|
15
|
+
const opts = { queryParams, method: 'post' };
|
|
16
|
+
request(f, '/account/validate', opts);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//
|
|
20
|
+
// Verify an account using the signed JWT token generate on account validation.
|
|
21
|
+
//
|
|
22
|
+
export function verifyAccount(f) {
|
|
23
|
+
return (params) => {
|
|
24
|
+
const opts = { queryParams: params };
|
|
25
|
+
request(f, '/account/verify', opts);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//
|
|
29
|
+
// Get user's informations associated to the bearer token.
|
|
30
|
+
//
|
|
31
|
+
export function getUserInfo(f) {
|
|
32
|
+
return async () => {
|
|
33
|
+
const opts = { parser: parseUserInfo };
|
|
34
|
+
return request(f, '/account/me', opts);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseUserInfo(data, _assoc) {
|
|
38
|
+
return {
|
|
39
|
+
name: data.name,
|
|
40
|
+
email: data.email
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const deviceFetchers: (f: typeof fetch) => {
|
|
2
|
+
registerDevice: (params: DeviceParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
3
|
+
};
|
|
4
|
+
export interface DeviceParams {
|
|
5
|
+
pushToken: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerDevice(f: typeof fetch): (params: DeviceParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { request } from '../utils/request';
|
|
2
|
+
export const deviceFetchers = (f) => {
|
|
3
|
+
return {
|
|
4
|
+
registerDevice: registerDevice(f),
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
export function registerDevice(f) {
|
|
8
|
+
return (params) => {
|
|
9
|
+
const body = { push_token: params.pushToken, name: params.name };
|
|
10
|
+
const opts = { body, method: 'post' };
|
|
11
|
+
return request(f, '/devices/register', opts);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ApiAssocs, type ApiData, type PagingParams } from '../utils/api';
|
|
2
|
+
import { type LocalesMetadata } from '../utils/response';
|
|
3
|
+
export declare const notificationFetchers: (f: typeof fetch) => {
|
|
4
|
+
listTopics: (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<TopicSummary[]>>;
|
|
5
|
+
subscribeToTopic: (params: SubscribtionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
6
|
+
unsubscribeFromTopic: (params: SubscribtionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
7
|
+
};
|
|
8
|
+
export declare function listTopics(f: typeof fetch): (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<TopicSummary[]>>;
|
|
9
|
+
export declare function parseTopicSummary(data: ApiData, _assocs: ApiAssocs): TopicSummary;
|
|
10
|
+
export interface TopicSummary {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
meta: {
|
|
14
|
+
locales: LocalesMetadata;
|
|
15
|
+
};
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
export interface SubscribtionParams {
|
|
19
|
+
topicId: string;
|
|
20
|
+
pushToken: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function subscribeToTopic(f: typeof fetch): (params: SubscribtionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
23
|
+
export declare function unsubscribeFromTopic(f: typeof fetch): (params: SubscribtionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { request } from '../utils/request';
|
|
2
|
+
import { parseMany } from '../utils/api';
|
|
3
|
+
import { parseLocalesMetadata } from '../utils/response';
|
|
4
|
+
export const notificationFetchers = (f) => {
|
|
5
|
+
return {
|
|
6
|
+
listTopics: listTopics(f),
|
|
7
|
+
subscribeToTopic: subscribeToTopic(f),
|
|
8
|
+
unsubscribeFromTopic: unsubscribeFromTopic(f),
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export function listTopics(f) {
|
|
12
|
+
return (params) => {
|
|
13
|
+
const opts = { queryParams: params, parser: parseMany(parseTopicSummary) };
|
|
14
|
+
return request(f, '/topics', opts);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function parseTopicSummary(data, _assocs) {
|
|
18
|
+
return {
|
|
19
|
+
id: data.id,
|
|
20
|
+
name: data.localized.name,
|
|
21
|
+
meta: {
|
|
22
|
+
locales: parseLocalesMetadata(data.meta)
|
|
23
|
+
},
|
|
24
|
+
updatedAt: data.updated_at
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function subscribeToTopic(f) {
|
|
28
|
+
return (params) => {
|
|
29
|
+
const body = { push_token: params.pushToken };
|
|
30
|
+
const opts = { method: 'post', body };
|
|
31
|
+
return request(f, '/topics/${params.topicId}/subscribe', opts);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function unsubscribeFromTopic(f) {
|
|
35
|
+
return (params) => {
|
|
36
|
+
const body = { push_token: params.pushToken };
|
|
37
|
+
const opts = { method: 'post', body };
|
|
38
|
+
return request(f, '/topics/${params.topicId}/unsubscribe', opts);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ApiAssocs, type ApiData, type PagingParams } from '../utils/api';
|
|
2
|
+
export declare const oauthFetchers: (f: typeof fetch) => {
|
|
3
|
+
listOauthProviders: (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<OauthProviderSummary[]>>;
|
|
4
|
+
getOauthUser: () => Promise<import("../utils/api").ApiResponse<OauthUser>>;
|
|
5
|
+
};
|
|
6
|
+
export declare function listOauthProviders(f: typeof fetch): (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<OauthProviderSummary[]>>;
|
|
7
|
+
export declare function parseOauthProviderSummary(data: ApiData, _assocs: ApiAssocs): OauthProviderSummary;
|
|
8
|
+
export interface OauthProviderSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
type: string;
|
|
12
|
+
authenticateUrl: string;
|
|
13
|
+
insertedAt: Date;
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
16
|
+
export declare function getOauthUser(f: typeof fetch): () => Promise<import("../utils/api").ApiResponse<OauthUser>>;
|
|
17
|
+
export declare function parseOauthUser(data: ApiData, _assoc: ApiAssocs): OauthUser;
|
|
18
|
+
export interface OauthUser {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
email: string;
|
|
22
|
+
verifiedAt: Date | null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { request } from '../utils/request';
|
|
2
|
+
import { parseMany } from '../utils/api';
|
|
3
|
+
export const oauthFetchers = (f) => {
|
|
4
|
+
return {
|
|
5
|
+
listOauthProviders: listOauthProviders(f),
|
|
6
|
+
getOauthUser: getOauthUser(f),
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export function listOauthProviders(f) {
|
|
10
|
+
return (params) => {
|
|
11
|
+
const opts = { queryParams: params, parser: parseMany(parseOauthProviderSummary) };
|
|
12
|
+
return request(f, '/oauth-providers', opts);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function parseOauthProviderSummary(data, _assocs) {
|
|
16
|
+
return {
|
|
17
|
+
id: data.id,
|
|
18
|
+
clientId: data.client_id,
|
|
19
|
+
type: data.type,
|
|
20
|
+
authenticateUrl: data.authenticate_url,
|
|
21
|
+
insertedAt: data.inserted_at,
|
|
22
|
+
updatedAt: data.updated_at
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function getOauthUser(f) {
|
|
26
|
+
return () => {
|
|
27
|
+
const opts = { parser: parseOauthUser };
|
|
28
|
+
return request(f, '/oauth/user', opts);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function parseOauthUser(data, _assoc) {
|
|
32
|
+
return {
|
|
33
|
+
id: data.id,
|
|
34
|
+
name: data.name,
|
|
35
|
+
email: data.email,
|
|
36
|
+
verifiedAt: data.verified_at
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const fetchers: (f: typeof fetch) => {
|
|
2
|
+
notifications: {
|
|
3
|
+
listTopics: (params?: Partial<import("./utils/api").PagingParams>) => Promise<import("./utils/api").ApiResponse<import("./endpoints/notification").TopicSummary[]>>;
|
|
4
|
+
subscribeToTopic: (params: import("./endpoints/notification").SubscribtionParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
5
|
+
unsubscribeFromTopic: (params: import("./endpoints/notification").SubscribtionParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
6
|
+
};
|
|
7
|
+
listOauthProviders: (params?: Partial<import("./utils/api").PagingParams>) => Promise<import("./utils/api").ApiResponse<import("./endpoints/oauth").OauthProviderSummary[]>>;
|
|
8
|
+
getOauthUser: () => Promise<import("./utils/api").ApiResponse<import("./endpoints/oauth").OauthUser>>;
|
|
9
|
+
registerDevice: (params: import("./endpoints/device").DeviceParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
10
|
+
userInfo: () => Promise<import("./utils/api").ApiResponse<import("./endpoints/accounts").UserInfo>>;
|
|
11
|
+
verifyAccount: (params: import("./endpoints/accounts").VerifyAccountParams) => void;
|
|
12
|
+
validateAccount: (params: import("./endpoints/accounts").ValidateAccountParams) => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { accountsFetchers } from './endpoints/accounts';
|
|
2
|
+
import { deviceFetchers } from './endpoints/device';
|
|
3
|
+
import { notificationFetchers } from './endpoints/notification';
|
|
4
|
+
import { oauthFetchers } from './endpoints/oauth';
|
|
5
|
+
export const fetchers = (f) => {
|
|
6
|
+
return {
|
|
7
|
+
...accountsFetchers(f),
|
|
8
|
+
...deviceFetchers(f),
|
|
9
|
+
...oauthFetchers(f),
|
|
10
|
+
notifications: notificationFetchers(f),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './stores/user_session';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { error } from "@sveltejs/kit";
|
|
2
|
+
import { env } from '$env/dynamic/private';
|
|
3
|
+
import { ApiError } from "../utils/api";
|
|
4
|
+
import { clearAuthorizationCookies, clearAuthorizationUsingHeader, getAccessTokenFromCookie, getApplicationToken, getRefreshTokenFromCookie, setAuthorizationCookies } from "./utils";
|
|
5
|
+
import { refresh } from "./token";
|
|
6
|
+
// NOTE: inspired by https://sami.website/blog/sveltekit-api-reverse-proxy
|
|
7
|
+
const handleApiProxy = async ({ event, ...tail }) => {
|
|
8
|
+
event.url.host = env.PRIVATE_OMERLO_HOST;
|
|
9
|
+
event.url.protocol = env.PRIVATE_OMERLO_PROTOCOL;
|
|
10
|
+
event.request.headers.delete('cookie');
|
|
11
|
+
event.request.headers.set('x-omerlo-media-id', env.PRIVATE_OMERLO_MEDIA_ID);
|
|
12
|
+
let accessToken = event.locals.accessToken;
|
|
13
|
+
if (!accessToken) {
|
|
14
|
+
accessToken = await getApplicationToken();
|
|
15
|
+
}
|
|
16
|
+
event.request.headers.set('Authorization', `Bearer ${accessToken}`);
|
|
17
|
+
return await fetch(event.url.toString(), {
|
|
18
|
+
body: event.request.body,
|
|
19
|
+
method: event.request.method,
|
|
20
|
+
headers: event.request.headers,
|
|
21
|
+
duplex: 'half'
|
|
22
|
+
})
|
|
23
|
+
.then(async (resp) => {
|
|
24
|
+
const headers = new Headers();
|
|
25
|
+
if (resp.status === 401 && event.locals.accessToken) {
|
|
26
|
+
clearAuthorizationUsingHeader(headers);
|
|
27
|
+
event.locals.accessToken = undefined;
|
|
28
|
+
resp = await handleApiProxy({ event, ...tail });
|
|
29
|
+
}
|
|
30
|
+
const responseOpts = {
|
|
31
|
+
headers: headers,
|
|
32
|
+
status: resp.status,
|
|
33
|
+
statusText: resp.statusText,
|
|
34
|
+
};
|
|
35
|
+
return new Response(resp.body, responseOpts);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
console.log("Could not proxy API request: ", err);
|
|
39
|
+
error(500, 'Something went wrong');
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export const proxyHook = async ({ event, resolve }) => {
|
|
43
|
+
if (event.url.pathname.startsWith('/api/media/v1')) {
|
|
44
|
+
return await handleApiProxy({ event, resolve });
|
|
45
|
+
}
|
|
46
|
+
return resolve(event);
|
|
47
|
+
};
|
|
48
|
+
export const handleUserToken = async ({ event, resolve }) => {
|
|
49
|
+
const accessToken = getAccessTokenFromCookie(event.cookies);
|
|
50
|
+
const refreshToken = getRefreshTokenFromCookie(event.cookies);
|
|
51
|
+
const opts = {
|
|
52
|
+
filterSerializedResponseHeaders: (name) => name == 'x-logout',
|
|
53
|
+
};
|
|
54
|
+
if (accessToken) {
|
|
55
|
+
event.locals.accessToken = accessToken;
|
|
56
|
+
return resolve(event, opts);
|
|
57
|
+
}
|
|
58
|
+
if (!refreshToken) {
|
|
59
|
+
return resolve(event, opts);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const token = await refresh(refreshToken);
|
|
63
|
+
setAuthorizationCookies(event.cookies, token);
|
|
64
|
+
event.locals.accessToken = token.accessToken;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof ApiError && err.status == 401) {
|
|
68
|
+
event.setHeaders({ 'x-logout': 'true' });
|
|
69
|
+
clearAuthorizationCookies(event.cookies);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return resolve(event);
|
|
73
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface exchangeAuthorizationCodeParams {
|
|
2
|
+
code: string;
|
|
3
|
+
redirectUri: string;
|
|
4
|
+
oauthProviderId: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Exchange an authorization code for an omerlo's token.
|
|
8
|
+
*/
|
|
9
|
+
export declare function exchangeAuthorizationCode(params: exchangeAuthorizationCodeParams): Promise<OmerloToken>;
|
|
10
|
+
/**
|
|
11
|
+
* Refresh the user's token.
|
|
12
|
+
*/
|
|
13
|
+
export declare function refresh(refreshToken: string): Promise<OmerloToken>;
|
|
14
|
+
/**
|
|
15
|
+
* Authenticate anonymously a user to get an anonymous token.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getAnonymousToken(scope: string): Promise<OmerloToken>;
|
|
18
|
+
export interface OmerloToken {
|
|
19
|
+
accessToken: string;
|
|
20
|
+
refreshToken: string;
|
|
21
|
+
expiresIn: number;
|
|
22
|
+
tokenType: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { env } from '$env/dynamic/private';
|
|
2
|
+
import { ApiError } from '../utils/api';
|
|
3
|
+
/**
|
|
4
|
+
* Exchange an authorization code for an omerlo's token.
|
|
5
|
+
*/
|
|
6
|
+
export async function exchangeAuthorizationCode(params) {
|
|
7
|
+
const url = getTokenEndpoint();
|
|
8
|
+
url.pathname = '/api/media/v1/oauth/token';
|
|
9
|
+
url.searchParams.append('client_id', env.PRIVATE_OMERLO_CLIENT_ID);
|
|
10
|
+
url.searchParams.append('grant_type', 'authorization_code');
|
|
11
|
+
url.searchParams.append('code', params.code);
|
|
12
|
+
url.searchParams.append('redirect_uri', params.redirectUri);
|
|
13
|
+
url.searchParams.append('oauth_provider_id', params.oauthProviderId);
|
|
14
|
+
const resp = await fetch(url, { method: 'POST' });
|
|
15
|
+
if (resp.ok) {
|
|
16
|
+
return await resp.json().then(parseTokenResponse);
|
|
17
|
+
}
|
|
18
|
+
const payload = await resp.json();
|
|
19
|
+
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Refresh the user's token.
|
|
23
|
+
*/
|
|
24
|
+
export async function refresh(refreshToken) {
|
|
25
|
+
const url = getTokenEndpoint();
|
|
26
|
+
url.pathname = '/api/media/v1/oauth/token';
|
|
27
|
+
url.searchParams.append('grant_type', 'refresh_token');
|
|
28
|
+
url.searchParams.append('refresh_token', refreshToken);
|
|
29
|
+
const resp = await fetch(url, { method: 'POST' });
|
|
30
|
+
if (resp.ok) {
|
|
31
|
+
return await resp.json().then(parseTokenResponse);
|
|
32
|
+
}
|
|
33
|
+
const payload = await resp.json();
|
|
34
|
+
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Authenticate anonymously a user to get an anonymous token.
|
|
38
|
+
*/
|
|
39
|
+
export async function getAnonymousToken(scope) {
|
|
40
|
+
const url = getTokenEndpoint();
|
|
41
|
+
url.pathname = '/api/media/v1/oauth/token';
|
|
42
|
+
url.searchParams.append('grant_type', 'client_credentials');
|
|
43
|
+
url.searchParams.append('scope', scope);
|
|
44
|
+
url.searchParams.append('client_id', env.PRIVATE_OMERLO_CLIENT_ID);
|
|
45
|
+
url.searchParams.append('client_secret', env.PRIVATE_OMERLO_CLIENT_SECRET);
|
|
46
|
+
const resp = await fetch(url, { method: 'POST' });
|
|
47
|
+
if (resp.ok) {
|
|
48
|
+
return await resp.json().then(parseTokenResponse);
|
|
49
|
+
}
|
|
50
|
+
const payload = await resp.json();
|
|
51
|
+
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
52
|
+
}
|
|
53
|
+
function getTokenEndpoint() {
|
|
54
|
+
return new URL(`${env.PRIVATE_OMERLO_PROTOCOL}://${env.PRIVATE_OMERLO_HOST}`);
|
|
55
|
+
}
|
|
56
|
+
function parseTokenResponse(data) {
|
|
57
|
+
return {
|
|
58
|
+
accessToken: data.access_token,
|
|
59
|
+
refreshToken: data.refresh_token,
|
|
60
|
+
expiresIn: data.expires_in,
|
|
61
|
+
tokenType: data.token_type,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UserSession } from "../stores/user_session";
|
|
2
|
+
import type { Cookies } from '@sveltejs/kit';
|
|
3
|
+
import { type OmerloToken } from "./token";
|
|
4
|
+
export declare function loadUserSession(f: typeof fetch, cookies: Cookies): Promise<UserSession>;
|
|
5
|
+
export declare function isAuthenticated(cookies: Cookies): boolean;
|
|
6
|
+
export declare function setAuthorizationCookies(cookies: Cookies, token: OmerloToken): void;
|
|
7
|
+
export declare function clearAuthorizationCookies(cookies: Cookies): void;
|
|
8
|
+
export declare function clearAuthorizationUsingHeader(headers: Headers): void;
|
|
9
|
+
export declare function getAccessTokenFromCookie(cookies: Cookies): string | null;
|
|
10
|
+
export declare function getRefreshTokenFromCookie(cookies: Cookies): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Get the token used by the application.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getApplicationToken(): Promise<string>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useReader } from "../..";
|
|
2
|
+
import { getAnonymousToken, refresh } from "./token";
|
|
3
|
+
export async function loadUserSession(f, cookies) {
|
|
4
|
+
const userSession = { verified: false, authenticated: false, user: null };
|
|
5
|
+
if (isAuthenticated(cookies)) {
|
|
6
|
+
userSession.authenticated = true;
|
|
7
|
+
try {
|
|
8
|
+
const userInfo = await useReader(f).userInfo().then((resp) => resp.data);
|
|
9
|
+
userSession.verified = true;
|
|
10
|
+
userSession.user = userInfo;
|
|
11
|
+
}
|
|
12
|
+
catch (_e) {
|
|
13
|
+
userSession.verified = false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return userSession;
|
|
17
|
+
}
|
|
18
|
+
export function isAuthenticated(cookies) {
|
|
19
|
+
return cookies.get('logged_in') == 'true';
|
|
20
|
+
}
|
|
21
|
+
const accessTokenCookieName = 'access_token';
|
|
22
|
+
const refreshTokenCookieName = 'refresh_token';
|
|
23
|
+
export function setAuthorizationCookies(cookies, token) {
|
|
24
|
+
cookies.set('logged_in', 'true', { path: '/', httpOnly: false });
|
|
25
|
+
cookies.set(accessTokenCookieName, token.accessToken, { httpOnly: true, path: '/', maxAge: token.expiresIn - 60 });
|
|
26
|
+
cookies.set(refreshTokenCookieName, token.refreshToken, { httpOnly: true, path: '/' });
|
|
27
|
+
}
|
|
28
|
+
export function clearAuthorizationCookies(cookies) {
|
|
29
|
+
cookies.delete(accessTokenCookieName, { path: '/' });
|
|
30
|
+
cookies.delete(refreshTokenCookieName, { path: '/' });
|
|
31
|
+
cookies.delete('logged_in', { path: '/', httpOnly: false });
|
|
32
|
+
}
|
|
33
|
+
export function clearAuthorizationUsingHeader(headers) {
|
|
34
|
+
headers.append('Set-Cookie', `${accessTokenCookieName}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`);
|
|
35
|
+
headers.append('Set-Cookie', `${refreshTokenCookieName}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`);
|
|
36
|
+
headers.append('Set-Cookie', `logged_in=; Path=/; Secure; SameSite=Lax; Max-Age=0`);
|
|
37
|
+
headers.append('x-logout', `true`);
|
|
38
|
+
}
|
|
39
|
+
export function getAccessTokenFromCookie(cookies) {
|
|
40
|
+
return cookies.get(accessTokenCookieName) || null;
|
|
41
|
+
}
|
|
42
|
+
export function getRefreshTokenFromCookie(cookies) {
|
|
43
|
+
return cookies.get(refreshTokenCookieName) || null;
|
|
44
|
+
}
|
|
45
|
+
const applicationToken = {
|
|
46
|
+
accessToken: '',
|
|
47
|
+
refreshToken: '',
|
|
48
|
+
expiredAt: 0,
|
|
49
|
+
init: false,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Get the token used by the application.
|
|
53
|
+
*/
|
|
54
|
+
export async function getApplicationToken() {
|
|
55
|
+
if (!applicationToken.init) {
|
|
56
|
+
await newApplicationToken();
|
|
57
|
+
}
|
|
58
|
+
else if (applicationToken.expiredAt < new Date().getTime()) {
|
|
59
|
+
await refreshApplicationToken();
|
|
60
|
+
}
|
|
61
|
+
return applicationToken.accessToken;
|
|
62
|
+
}
|
|
63
|
+
async function refreshApplicationToken() {
|
|
64
|
+
if (!applicationToken.refreshToken) {
|
|
65
|
+
throw new Error('Could not refresh the application token because the refresh token is null');
|
|
66
|
+
}
|
|
67
|
+
const token = await refresh(applicationToken.refreshToken);
|
|
68
|
+
applicationToken.accessToken = token.accessToken;
|
|
69
|
+
applicationToken.refreshToken = token.refreshToken;
|
|
70
|
+
const date = new Date();
|
|
71
|
+
const timestamps = date.setSeconds(date.getSeconds() + token.expiresIn - 60);
|
|
72
|
+
applicationToken.expiredAt = timestamps;
|
|
73
|
+
}
|
|
74
|
+
async function newApplicationToken() {
|
|
75
|
+
const token = await getAnonymousToken('application');
|
|
76
|
+
applicationToken.init = true;
|
|
77
|
+
applicationToken.accessToken = token.accessToken;
|
|
78
|
+
applicationToken.refreshToken = token.refreshToken;
|
|
79
|
+
const date = new Date();
|
|
80
|
+
const timestamps = date.setSeconds(date.getSeconds() + token.expiresIn - 60);
|
|
81
|
+
applicationToken.expiredAt = timestamps;
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UserInfo } from "../endpoints/accounts";
|
|
2
|
+
import { type Readable } from "svelte/store";
|
|
3
|
+
export interface UserSession {
|
|
4
|
+
user: UserInfo | null;
|
|
5
|
+
verified: boolean;
|
|
6
|
+
authenticated: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function initUserSession(session: UserSession): ReadableUserSession;
|
|
9
|
+
interface ReadableUserSession extends Readable<UserSession> {
|
|
10
|
+
refresh: () => void;
|
|
11
|
+
handleLogout: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function getUserSession(): ReadableUserSession;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
import { writable } from "svelte/store";
|
|
3
|
+
import { browser } from "$app/environment";
|
|
4
|
+
import { useReader } from "../..";
|
|
5
|
+
import { invalidate } from "$app/navigation";
|
|
6
|
+
const anonymousUserSession = {
|
|
7
|
+
user: null,
|
|
8
|
+
verified: false,
|
|
9
|
+
authenticated: false,
|
|
10
|
+
};
|
|
11
|
+
export function initUserSession(session) {
|
|
12
|
+
const { subscribe, update, set } = writable(session);
|
|
13
|
+
if (browser) {
|
|
14
|
+
localStorage.setItem('user_session', JSON.stringify(session));
|
|
15
|
+
addEventListener('storage', (event) => {
|
|
16
|
+
if (event.key != 'user_session')
|
|
17
|
+
return;
|
|
18
|
+
if (!event.newValue) {
|
|
19
|
+
set(anonymousUserSession);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const userSession = JSON.parse(event.newValue);
|
|
23
|
+
set(userSession);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const ctx = {
|
|
28
|
+
subscribe,
|
|
29
|
+
handleLogout: () => {
|
|
30
|
+
if (!browser) {
|
|
31
|
+
throw new Error("MUST NOT call refresh on user session from server side.");
|
|
32
|
+
}
|
|
33
|
+
invalidate('omerlo:user_session');
|
|
34
|
+
update(updateUserInfo(null, false));
|
|
35
|
+
},
|
|
36
|
+
refresh: async () => {
|
|
37
|
+
if (!browser) {
|
|
38
|
+
throw new Error("MUST NOT call refresh on user session from server side.");
|
|
39
|
+
}
|
|
40
|
+
const userInfo = await useReader(fetch).userInfo().then((resp) => resp.data);
|
|
41
|
+
update(updateUserInfo(userInfo, true));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
setContext('user_session', ctx);
|
|
45
|
+
return ctx;
|
|
46
|
+
}
|
|
47
|
+
const updateUserInfo = (userInfo, authenticated) => {
|
|
48
|
+
return (session) => {
|
|
49
|
+
session.user = userInfo;
|
|
50
|
+
session.verified = userInfo != null;
|
|
51
|
+
session.authenticated = authenticated;
|
|
52
|
+
localStorage.setItem('user_session', JSON.stringify(session));
|
|
53
|
+
return session;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export function getUserSession() {
|
|
57
|
+
return getContext('user_session');
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Assoc } from './assocs';
|
|
2
|
+
export type ApiAssocs = Record<string, Record<string, Assoc>>;
|
|
3
|
+
export declare function parseApiResponse<T>(response: Response, parser: (data: ApiData, assocs: ApiAssocs) => T): Promise<ApiResponse<T>>;
|
|
4
|
+
export declare function parseMany<T>(parser: (data: ApiData, assocs: ApiAssocs) => T): (response: ApiData[], assocs: ApiAssocs) => T[];
|
|
5
|
+
export interface ApiResponse<T> {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
errors: ApiData[];
|
|
8
|
+
status: number;
|
|
9
|
+
parser: (data: ApiData, assocs: ApiAssocs) => T;
|
|
10
|
+
data: T | null;
|
|
11
|
+
meta: ApiResponseMeta | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ApiResponseMeta {
|
|
14
|
+
next: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ApiParams {
|
|
17
|
+
locale?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface PagingParams extends ApiParams {
|
|
20
|
+
limit?: number | null;
|
|
21
|
+
after?: string | null;
|
|
22
|
+
before?: string | null;
|
|
23
|
+
}
|
|
24
|
+
export type ApiData = any;
|
|
25
|
+
export declare class ApiError extends Error {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly payload: any;
|
|
28
|
+
constructor(status: number, payload: any, ...params: any[]);
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseAssocs } from './assocs';
|
|
2
|
+
export async function parseApiResponse(response, parser) {
|
|
3
|
+
const payload = await response.json();
|
|
4
|
+
let data = null, meta = null;
|
|
5
|
+
if (response.ok) {
|
|
6
|
+
parseAssocs(payload.assocs);
|
|
7
|
+
meta = payload.meta;
|
|
8
|
+
data = parser(payload.data, payload.assocs);
|
|
9
|
+
}
|
|
10
|
+
const errors = payload.errors || [];
|
|
11
|
+
return { ok: response.ok, status: response.status, parser, meta, data, errors };
|
|
12
|
+
}
|
|
13
|
+
export function parseMany(parser) {
|
|
14
|
+
return (response, assocs) => {
|
|
15
|
+
return response.map((data) => parser(data, assocs));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export class ApiError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
payload;
|
|
21
|
+
constructor(status, payload, ...params) {
|
|
22
|
+
super(...params);
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.payload = payload;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ApiAssocs } from './api';
|
|
2
|
+
export declare function registerAssocParser(assocName: string, parser: (arg: unknown) => unknown): void;
|
|
3
|
+
/**
|
|
4
|
+
* Return the assoc corresponding the given `id`
|
|
5
|
+
*/
|
|
6
|
+
export declare function getAssoc<T>(assocs: ApiAssocs, name: string, id: string): T;
|
|
7
|
+
/**
|
|
8
|
+
* Return assocs corresponding given `ids`
|
|
9
|
+
*/
|
|
10
|
+
export declare function getAssocs<T>(assocs: ApiAssocs, name: string, ids: string[]): T[];
|
|
11
|
+
/**
|
|
12
|
+
* Parse all assocs using an ordering system to prevent any clone.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseAssocs(apiAssocs: ApiAssocs): void;
|
|
15
|
+
export type Assoc = unknown;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const assocsParsers = {};
|
|
2
|
+
export function registerAssocParser(assocName, parser) {
|
|
3
|
+
assocsParsers[assocName] = parser;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Return the assoc corresponding the given `id`
|
|
7
|
+
*/
|
|
8
|
+
export function getAssoc(assocs, name, id) {
|
|
9
|
+
const assoc = assocs[name]?.[id];
|
|
10
|
+
if (assoc == undefined) {
|
|
11
|
+
console.error(`Assoc ${name} not found`);
|
|
12
|
+
}
|
|
13
|
+
return assoc;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Return assocs corresponding given `ids`
|
|
17
|
+
*/
|
|
18
|
+
export function getAssocs(assocs, name, ids) {
|
|
19
|
+
return ids.map((id) => getAssoc(assocs, name, id));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse all assocs using an ordering system to prevent any clone.
|
|
23
|
+
*/
|
|
24
|
+
export function parseAssocs(apiAssocs) {
|
|
25
|
+
for (const assocName in apiAssocs) {
|
|
26
|
+
const assocs = apiAssocs[assocName];
|
|
27
|
+
for (const assocId in assocs) {
|
|
28
|
+
const assoc = assocs[assocId];
|
|
29
|
+
assocs[assocId] = assocsParsers[assocName](assoc, apiAssocs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ApiAssocs, ApiData, ApiResponse } from './api';
|
|
2
|
+
type FetchOptions<T> = {
|
|
3
|
+
parser?: (data: ApiData, assocs: ApiAssocs) => T;
|
|
4
|
+
queryParams?: ApiData;
|
|
5
|
+
method?: string;
|
|
6
|
+
body?: ApiData;
|
|
7
|
+
};
|
|
8
|
+
export declare function request<T>(f: typeof fetch, path: string, opts: FetchOptions<T>): Promise<ApiResponse<T>>;
|
|
9
|
+
type DirtyFetchOptions = {
|
|
10
|
+
queryParams?: ApiData;
|
|
11
|
+
method?: string;
|
|
12
|
+
body?: ApiData;
|
|
13
|
+
};
|
|
14
|
+
export declare function dirtyRequest(f: typeof fetch, path: string, opts?: DirtyFetchOptions): Promise<Response>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseApiResponse } from './api';
|
|
2
|
+
import { BROWSER } from 'esm-env';
|
|
3
|
+
export async function request(f, path, opts) {
|
|
4
|
+
const parser = opts.parser || ((data) => data);
|
|
5
|
+
return dirtyRequest(f, path, opts).then(async (resp) => {
|
|
6
|
+
return parseApiResponse(resp, parser);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function dirtyRequest(f, path, opts) {
|
|
10
|
+
const queryParams = new URLSearchParams();
|
|
11
|
+
path = `/api/media/v1${path}`;
|
|
12
|
+
if (opts?.queryParams) {
|
|
13
|
+
Object.entries(opts.queryParams).forEach(([key, value]) => {
|
|
14
|
+
queryParams.append(key, String(value));
|
|
15
|
+
});
|
|
16
|
+
path = `${path}?${queryParams}`;
|
|
17
|
+
}
|
|
18
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
19
|
+
const resp = await f(path.toString(), { method: opts.method, body: JSON.stringify(opts.body), headers });
|
|
20
|
+
if (BROWSER && resp.headers.get('x-logout') == 'true') {
|
|
21
|
+
const webkitComponent = document.getElementById('omerlo-webkit');
|
|
22
|
+
if (webkitComponent) {
|
|
23
|
+
webkitComponent.dispatchEvent(new Event('logout'));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return resp;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Reader from './reader/server';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Reader from './reader/server';
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import prettier from 'eslint-config-prettier';
|
|
2
|
+
import js from '@eslint/js';
|
|
3
|
+
import svelte from 'eslint-plugin-svelte';
|
|
4
|
+
import ts from 'typescript-eslint';
|
|
5
|
+
import globals from 'globals';
|
|
6
|
+
|
|
7
|
+
export default ts.config(
|
|
8
|
+
js.configs.recommended,
|
|
9
|
+
...ts.configs.recommended,
|
|
10
|
+
...svelte.configs['flat/recommended'],
|
|
11
|
+
prettier,
|
|
12
|
+
...svelte.configs['flat/prettier'],
|
|
13
|
+
{
|
|
14
|
+
files: ['**/*.svelte'],
|
|
15
|
+
|
|
16
|
+
languageOptions: {
|
|
17
|
+
globals: {
|
|
18
|
+
...globals.browser,
|
|
19
|
+
...globals.node,
|
|
20
|
+
},
|
|
21
|
+
parserOptions: {
|
|
22
|
+
parser: ts.parser
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
ignores: ['build/', '.svelte-kit/', 'dist/']
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
rules: {
|
|
31
|
+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omerlo/omerlo-webkit",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "vite dev",
|
|
6
|
+
"build": "vite build && npm run package",
|
|
7
|
+
"preview": "vite preview",
|
|
8
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
9
|
+
"prepublishOnly": "npm run package",
|
|
10
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"lint": "prettier --check . && eslint ."
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"!dist/**/*.test.*",
|
|
18
|
+
"!dist/**/*.spec.*"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"**/*.css"
|
|
22
|
+
],
|
|
23
|
+
"svelte": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"svelte": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./reader": {
|
|
32
|
+
"types": "./dist/omerlo/reader/index.d.ts",
|
|
33
|
+
"svelte": "./dist/omerlo/reader/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./reader/server": {
|
|
36
|
+
"types": "./dist/omerlo/reader/server/index.d.ts",
|
|
37
|
+
"svelte": "./dist/omerlo/reader/server/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@sveltejs/kit": "^2.0.0",
|
|
42
|
+
"svelte": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@sveltejs/adapter-auto": "^3.0.0",
|
|
46
|
+
"@sveltejs/kit": "^2.0.0",
|
|
47
|
+
"@sveltejs/package": "^2.0.0",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
49
|
+
"@types/eslint": "^9.6.0",
|
|
50
|
+
"@types/eslint-config-prettier": "^6.11.3",
|
|
51
|
+
"autoprefixer": "^10.4.20",
|
|
52
|
+
"eslint": "^9.7.0",
|
|
53
|
+
"eslint-config-prettier": "^9.1.0",
|
|
54
|
+
"eslint-plugin-svelte": "^2.36.0",
|
|
55
|
+
"globals": "^15.0.0",
|
|
56
|
+
"prettier": "^3.3.2",
|
|
57
|
+
"prettier-plugin-svelte": "^3.2.6",
|
|
58
|
+
"prettier-plugin-tailwindcss": "^0.6.5",
|
|
59
|
+
"publint": "^0.2.0",
|
|
60
|
+
"svelte": "^5.0.0",
|
|
61
|
+
"svelte-check": "^4.0.0",
|
|
62
|
+
"typescript": "^5.0.0",
|
|
63
|
+
"typescript-eslint": "^8.0.0",
|
|
64
|
+
"vite": "^5.0.11"
|
|
65
|
+
},
|
|
66
|
+
"description": "This webkit is a wapper arround Omerlo's API to create quickly a website using Omerlo.",
|
|
67
|
+
"main": "eslint.config.js",
|
|
68
|
+
"directories": { },
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/Omerlo-Technologies/omerlo-webkit.git"
|
|
72
|
+
},
|
|
73
|
+
"author": "Omerlo Technologies",
|
|
74
|
+
"license": "ISC",
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/Omerlo-Technologies/omerlo-webkit/issues"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/Omerlo-Technologies/omerlo-webkit#readme"
|
|
79
|
+
}
|