@pinooxhq/auth 0.1.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/LICENSE +21 -0
- package/README.md +705 -0
- package/dist/createAuth-LO46VVEW.d.cts +176 -0
- package/dist/createAuth-LO46VVEW.d.ts +176 -0
- package/dist/index.cjs +853 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +836 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +862 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +38 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +857 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.cjs +922 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +60 -0
- package/dist/svelte/index.d.ts +60 -0
- package/dist/svelte/index.js +915 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +877 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +71 -0
- package/dist/vue/index.d.ts +71 -0
- package/dist/vue/index.js +871 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
# `@pinooxhq/auth`
|
|
2
|
+
|
|
3
|
+
Auth for Pinoox themes — **configured in PHP, zero hardcoding in the SPA**.
|
|
4
|
+
|
|
5
|
+
Token storage, authorized HTTP, `me()` / logout, and login redirects for **Vue**, **React**, and **Svelte**. Values come from `window.__PINOOX__.auth` (published by `auth.client` in `app.php`).
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
1. [Schematics & how it works](#1-schematics--how-it-works) ← **start here**
|
|
10
|
+
2. [Setup guide — Independent](#2-setup-guide--independent)
|
|
11
|
+
3. [Setup guide — SSO](#3-setup-guide--sso)
|
|
12
|
+
4. [Platform variant](#4-platform-variant)
|
|
13
|
+
5. [Quick theme module](#5-quick-theme-module)
|
|
14
|
+
6. [Why `auth.client`?](#6-why-authclient)
|
|
15
|
+
7. [App configuration reference](#7-app-configuration-reference)
|
|
16
|
+
8. [Theme integration](#8-theme-integration)
|
|
17
|
+
9. [HTTP, redirects, adapters](#9-http-redirects-adapters)
|
|
18
|
+
10. [API reference](#10-api-reference)
|
|
19
|
+
11. [Vite](#11-vite)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Schematics & how it works
|
|
24
|
+
|
|
25
|
+
Two layers always work together:
|
|
26
|
+
|
|
27
|
+
| Layer | Where | What it controls |
|
|
28
|
+
| ----- | ----- | ---------------- |
|
|
29
|
+
| **`transport`** | `app.php` | Which app owns **users / JWT secret / token key** on the server |
|
|
30
|
+
| **`auth.client`** | `app.php` → `__PINOOX__.auth` | What the **SPA** knows: strategy, login URL, API endpoints |
|
|
31
|
+
|
|
32
|
+
### Independent mode
|
|
33
|
+
|
|
34
|
+
Each app logs users in by itself. Tokens are **not** shared.
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart LR
|
|
38
|
+
User --> Shop
|
|
39
|
+
User --> CRM
|
|
40
|
+
Shop -.- Sown[own login + token]
|
|
41
|
+
CRM -.- Cown[own login + token]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**How it works**
|
|
45
|
+
|
|
46
|
+
1. `app.php`: `auth.mode` + `auth.key` + `client => true` (no `transport`).
|
|
47
|
+
2. Strategy is **`local`** — login UI lives in this app.
|
|
48
|
+
3. Login issues a JWT stored under that app’s `auth.key`.
|
|
49
|
+
4. `me` / `logout` hit **this** app’s API only.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### SSO mode (one login + dependents)
|
|
54
|
+
|
|
55
|
+
One app (`com_pinoox_account`) is the login. Shop and CRM reuse the **same** session.
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
flowchart LR
|
|
59
|
+
Shop -->|no token| Account
|
|
60
|
+
CRM -->|no token| Account
|
|
61
|
+
Account -->|shared token| Shop
|
|
62
|
+
Account -->|shared token| CRM
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**How it works**
|
|
66
|
+
|
|
67
|
+
1. **Account** owns users + `auth.key` (local login).
|
|
68
|
+
2. **Shop / CRM**: `transport.user => com_pinoox_account` + `strategy: remote`.
|
|
69
|
+
3. Guest → redirect to Account → login → back with `?redirect=`.
|
|
70
|
+
4. Same browser key unlocks every dependent app.
|
|
71
|
+
|
|
72
|
+
| | Account | Shop / CRM |
|
|
73
|
+
| - | ------- | ---------- |
|
|
74
|
+
| Login | Yes | No |
|
|
75
|
+
| Token key | Defines it | Shared via `transport` |
|
|
76
|
+
| Strategy | `local` | `remote` |
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. Setup guide — Independent
|
|
80
|
+
|
|
81
|
+
Goal: Shop has its own login. CRM is unrelated (or also independent with a different key).
|
|
82
|
+
|
|
83
|
+
### Step 1 — `app.php`
|
|
84
|
+
|
|
85
|
+
```php
|
|
86
|
+
// apps/com_pinoox_shop/app.php
|
|
87
|
+
return [
|
|
88
|
+
'package' => 'com_pinoox_shop',
|
|
89
|
+
// … theme, router, …
|
|
90
|
+
|
|
91
|
+
// no 'transport' → local users + local auth
|
|
92
|
+
|
|
93
|
+
'auth' => [
|
|
94
|
+
'mode' => 'jwt',
|
|
95
|
+
'key' => 'pinoox_shop_auth',
|
|
96
|
+
'lifetime' => 30,
|
|
97
|
+
'lifetime_unit' => 'day',
|
|
98
|
+
'client' => true,
|
|
99
|
+
],
|
|
100
|
+
];
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
php pinoox pinker:rebuild com_pinoox_shop -n
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Step 2 — Theme auth module
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cd apps/com_pinoox_shop/theme/default
|
|
111
|
+
npm install @pinooxhq/auth axios
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// src/utils/auth/index.js
|
|
116
|
+
import axios from 'axios'
|
|
117
|
+
import { defineStore } from 'pinia'
|
|
118
|
+
import { createAuth, createHttp } from '@pinooxhq/auth'
|
|
119
|
+
import { createPiniaAuthStore } from '@pinooxhq/auth/vue'
|
|
120
|
+
|
|
121
|
+
export const auth = createAuth({ debug: import.meta.env.DEV })
|
|
122
|
+
export const http = createHttp({ auth, axios })
|
|
123
|
+
export const useAuthStore = createPiniaAuthStore(defineStore, 'auth')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Step 3 — Login page
|
|
127
|
+
|
|
128
|
+
```vue
|
|
129
|
+
<script setup>
|
|
130
|
+
import { http, useAuthStore } from '@/utils/auth'
|
|
131
|
+
import { useAuthRedirect } from '@pinooxhq/auth/vue'
|
|
132
|
+
|
|
133
|
+
const store = useAuthStore()
|
|
134
|
+
const { redirectBack } = useAuthRedirect()
|
|
135
|
+
|
|
136
|
+
const onLogin = async () => {
|
|
137
|
+
const { data } = await http.post('auth/login', { username, password })
|
|
138
|
+
const body = data?.data ?? data
|
|
139
|
+
store.login(body.token, body.user)
|
|
140
|
+
redirectBack()
|
|
141
|
+
}
|
|
142
|
+
</script>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Step 4 — Guard
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { useAuthStore } from '@/utils/auth'
|
|
149
|
+
|
|
150
|
+
export async function authGuard(to, _from, next) {
|
|
151
|
+
const store = useAuthStore()
|
|
152
|
+
store.syncTokenFromStorage()
|
|
153
|
+
if (!store.isAuth) await store.canUserAccess(true)
|
|
154
|
+
if (to.meta.requiresAuth && !store.isAuth) {
|
|
155
|
+
return next({ name: 'login', query: { redirect: to.fullPath } })
|
|
156
|
+
}
|
|
157
|
+
next()
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Checklist
|
|
162
|
+
|
|
163
|
+
- [ ] `auth.mode` + `auth.key` on this app
|
|
164
|
+
- [ ] `auth.client => true`
|
|
165
|
+
- [ ] Login UI in **this** theme
|
|
166
|
+
- [ ] Do **not** set `strategy: remote`
|
|
167
|
+
- [ ] Twig: `pinoox_bootstrap` before `vite_tags`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 3. Setup guide — SSO
|
|
172
|
+
|
|
173
|
+
Goal: Account is the only login. Shop and CRM reuse one session.
|
|
174
|
+
|
|
175
|
+
### Step 1 — Provider (`com_pinoox_account`)
|
|
176
|
+
|
|
177
|
+
```php
|
|
178
|
+
// apps/com_pinoox_account/app.php
|
|
179
|
+
'auth' => [
|
|
180
|
+
'mode' => 'jwt',
|
|
181
|
+
'key' => 'pinoox_ecosystem', // shared by every dependent
|
|
182
|
+
'client' => true, // local strategy
|
|
183
|
+
],
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Account theme: normal **local** login (same as Independent Step 3), then `redirectBack()` so `?redirect=` returns the user to Shop/CRM.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
php pinoox pinker:rebuild com_pinoox_account -n
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Step 2 — Dependent Shop
|
|
193
|
+
|
|
194
|
+
```php
|
|
195
|
+
// apps/com_pinoox_shop/app.php
|
|
196
|
+
'depends' => ['com_pinoox_account'],
|
|
197
|
+
|
|
198
|
+
'transport' => [
|
|
199
|
+
'user' => 'com_pinoox_account', // inherit users + JWT secret + key
|
|
200
|
+
],
|
|
201
|
+
|
|
202
|
+
'auth' => [
|
|
203
|
+
'client' => [
|
|
204
|
+
'strategy' => 'remote',
|
|
205
|
+
'loginUrl' => '/account/login',
|
|
206
|
+
'baseUrl' => '/account/api/v1',
|
|
207
|
+
'endpoints' => [
|
|
208
|
+
'login' => 'auth/login',
|
|
209
|
+
'logout' => 'auth/logout',
|
|
210
|
+
'me' => 'auth/get',
|
|
211
|
+
],
|
|
212
|
+
],
|
|
213
|
+
],
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Step 3 — Dependent CRM (same pattern)
|
|
217
|
+
|
|
218
|
+
```php
|
|
219
|
+
// apps/com_pinoox_crm/app.php
|
|
220
|
+
'depends' => ['com_pinoox_account'],
|
|
221
|
+
'transport' => ['user' => 'com_pinoox_account'],
|
|
222
|
+
'auth' => [
|
|
223
|
+
'client' => [
|
|
224
|
+
'strategy' => 'remote',
|
|
225
|
+
'loginUrl' => '/account/login',
|
|
226
|
+
'baseUrl' => '/account/api/v1',
|
|
227
|
+
'endpoints' => [
|
|
228
|
+
'me' => 'auth/get',
|
|
229
|
+
'logout' => 'auth/logout',
|
|
230
|
+
],
|
|
231
|
+
],
|
|
232
|
+
],
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
php pinoox pinker:rebuild com_pinoox_shop -n
|
|
237
|
+
php pinoox pinker:rebuild com_pinoox_crm -n
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Step 4 — Theme on each dependent
|
|
241
|
+
|
|
242
|
+
Same `utils/auth` as Independent — **no hardcoded key**. `createAuth()` reads remote settings from `__PINOOX__.auth`.
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
export const auth = createAuth({ debug: import.meta.env.DEV })
|
|
246
|
+
export const http = createHttp({ auth, axios })
|
|
247
|
+
export const useAuthStore = createPiniaAuthStore(defineStore, 'auth')
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Guard — redirect to Account when guest:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import { auth, useAuthStore } from '@/utils/auth'
|
|
254
|
+
|
|
255
|
+
export async function authGuard(to, _from, next) {
|
|
256
|
+
const store = useAuthStore()
|
|
257
|
+
store.syncTokenFromStorage()
|
|
258
|
+
if (!store.isAuth) await store.canUserAccess(true)
|
|
259
|
+
if (to.meta.requiresAuth && !store.isAuth) {
|
|
260
|
+
auth.redirectToLogin() // → /account/login?redirect=…
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
next()
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Do **not** build a login form on Shop/CRM for SSO — only Account has the form.
|
|
268
|
+
|
|
269
|
+
### Checklist (dependent)
|
|
270
|
+
|
|
271
|
+
- [ ] `transport.user` → `com_pinoox_account`
|
|
272
|
+
- [ ] `auth.client.strategy` = `remote`
|
|
273
|
+
- [ ] `loginUrl` = `/account/login`
|
|
274
|
+
- [ ] `baseUrl` + `endpoints` for `me` / `logout`
|
|
275
|
+
- [ ] Do **not** set a different `auth.key` (unless you want a separate session)
|
|
276
|
+
- [ ] Pinker rebuild after `app.php` changes
|
|
277
|
+
|
|
278
|
+
### Side-by-side
|
|
279
|
+
|
|
280
|
+
| | Independent | SSO |
|
|
281
|
+
| - | ----------- | --- |
|
|
282
|
+
| Example | Shop alone; CRM alone | Account ← Shop, CRM |
|
|
283
|
+
| `transport.user` | — | `com_pinoox_account` |
|
|
284
|
+
| Login UI | Each app | Account only |
|
|
285
|
+
| Browser key | Per app (`pinoox_shop_auth`, …) | One key (`pinoox_ecosystem`) |
|
|
286
|
+
| `auth.client` | `true` | map with `strategy: remote` |
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 4. Platform variant
|
|
291
|
+
|
|
292
|
+
Same idea as SSO, but the login owner is the **platform** auth app (Manager), not Account. Use for admin tools that must not share the customer Account session.
|
|
293
|
+
|
|
294
|
+
```mermaid
|
|
295
|
+
flowchart LR
|
|
296
|
+
CRM -->|no token| Manager
|
|
297
|
+
Inventory -->|no token| Manager
|
|
298
|
+
Manager -->|one token| CRM
|
|
299
|
+
Manager -->|one token| Inventory
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```php
|
|
303
|
+
// dependent admin app
|
|
304
|
+
'transport' => ['user' => 'platform'],
|
|
305
|
+
'auth' => [
|
|
306
|
+
'client' => [
|
|
307
|
+
'strategy' => 'remote',
|
|
308
|
+
'loginUrl' => '/manager/login',
|
|
309
|
+
'baseUrl' => '/crm/api/v1',
|
|
310
|
+
'endpoints' => [
|
|
311
|
+
'me' => 'auth/get',
|
|
312
|
+
'logout' => 'auth/logout',
|
|
313
|
+
],
|
|
314
|
+
],
|
|
315
|
+
],
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| `transport.user` | Auth / users come from |
|
|
319
|
+
| ---------------- | ---------------------- |
|
|
320
|
+
| omitted / `local` | This app only (independent) |
|
|
321
|
+
| `'com_pinoox_account'` | Account SSO |
|
|
322
|
+
| `'platform'` | Platform / Manager SSO |
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 5. Quick theme module
|
|
327
|
+
|
|
328
|
+
Same for all topologies — only `__PINOOX__.auth` changes.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
cd apps/com_pinoox_shop/theme/default
|
|
332
|
+
npm install @pinooxhq/auth axios
|
|
333
|
+
php pinoox pinker:rebuild com_pinoox_shop -n
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`src/utils/auth/index.js`:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import axios from 'axios'
|
|
340
|
+
import { defineStore } from 'pinia'
|
|
341
|
+
import { createAuth, createHttp } from '@pinooxhq/auth'
|
|
342
|
+
import { createPiniaAuthStore } from '@pinooxhq/auth/vue'
|
|
343
|
+
|
|
344
|
+
export const auth = createAuth({ debug: import.meta.env.DEV })
|
|
345
|
+
export const http = createHttp({ auth, axios })
|
|
346
|
+
export const useAuthStore = createPiniaAuthStore(defineStore, 'auth')
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
import { auth, useAuthStore } from '@/utils/auth'
|
|
351
|
+
|
|
352
|
+
const store = useAuthStore()
|
|
353
|
+
await store.canUserAccess(true)
|
|
354
|
+
|
|
355
|
+
if (!store.isAuth) {
|
|
356
|
+
auth.redirectToLogin() // remote → loginUrl; local → your login route
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 6. Why `auth.client`?
|
|
363
|
+
|
|
364
|
+
PHP already owns `mode`, `key`, JWT secret, and login URLs. The SPA must not invent them.
|
|
365
|
+
|
|
366
|
+
`auth.client` publishes a **safe subset** to `window.__PINOOX__.auth` (via `pinoox_bootstrap()`).
|
|
367
|
+
|
|
368
|
+
| Stays on server | May go to the client |
|
|
369
|
+
| --------------- | -------------------- |
|
|
370
|
+
| `jwt_secret`, lifetimes, … | `mode`, `key`, `provider`, `source` |
|
|
371
|
+
| | optional: `strategy`, `loginUrl`, `baseUrl`, `endpoints` |
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
app.php auth.client → pinker + pinoox_bootstrap() → __PINOOX__.auth
|
|
375
|
+
↓
|
|
376
|
+
createAuth() / createHttp()
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Twig (order matters):
|
|
380
|
+
|
|
381
|
+
```twig
|
|
382
|
+
{{ pinoox_bootstrap(bootstrap|default({}))|raw }}
|
|
383
|
+
{{ vite_tags() }}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### `auth.client` — what gets into `__PINOOX__.auth`
|
|
387
|
+
|
|
388
|
+
The server always resolves a **base** payload from auth + transport:
|
|
389
|
+
|
|
390
|
+
```php
|
|
391
|
+
// Always available on the server (AuthConfig::forClient base)
|
|
392
|
+
[
|
|
393
|
+
'mode' => 'jwt', // jwt | cookie | session
|
|
394
|
+
'key' => 'pinoox_ecosystem', // localStorage / cookie name
|
|
395
|
+
'provider' => 'com_pinoox_account', // package that owns auth (or this app)
|
|
396
|
+
'source' => 'com_pinoox_account', // transport auth source (or null)
|
|
397
|
+
]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`auth.client` decides **how much of that (and extras) the browser may see**.
|
|
401
|
+
|
|
402
|
+
#### 1) `true` — publish the full base (default)
|
|
403
|
+
|
|
404
|
+
```php
|
|
405
|
+
// e.g. com_pinoox_shop — independent login
|
|
406
|
+
'auth' => [
|
|
407
|
+
'mode' => 'jwt',
|
|
408
|
+
'key' => 'pinoox_shop_auth',
|
|
409
|
+
'client' => true,
|
|
410
|
+
],
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
// window.__PINOOX__.auth
|
|
415
|
+
{ mode: 'jwt', key: 'pinoox_shop_auth', provider: 'com_pinoox_shop', source: null }
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Use for a normal local login app. `createAuth()` gets mode + key with no JS hardcoding.
|
|
419
|
+
|
|
420
|
+
#### 2) `false` — publish nothing
|
|
421
|
+
|
|
422
|
+
```php
|
|
423
|
+
'auth' => [
|
|
424
|
+
'mode' => 'jwt',
|
|
425
|
+
'key' => 'pinoox_crm_auth',
|
|
426
|
+
'client' => false,
|
|
427
|
+
],
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`__PINOOX__.auth` is omitted. You must pass everything in JS:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
createAuth({ mode: 'jwt', key: 'pinoox_crm_auth', strategy: 'local' })
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Use only when you deliberately keep auth config off the page (or hydrate from elsewhere).
|
|
437
|
+
|
|
438
|
+
#### 3) List — whitelist of base fields only
|
|
439
|
+
|
|
440
|
+
A **PHP list** (`array_is_list`) means: from the base payload, keep **only** these keys. Nothing else is added.
|
|
441
|
+
|
|
442
|
+
```php
|
|
443
|
+
// Account (or Shop) — only mode + key in the page; hide provider/source package names
|
|
444
|
+
'auth' => [
|
|
445
|
+
'mode' => 'jwt',
|
|
446
|
+
'key' => 'pinoox_ecosystem',
|
|
447
|
+
'client' => ['mode', 'key'],
|
|
448
|
+
],
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
```js
|
|
452
|
+
// window.__PINOOX__.auth
|
|
453
|
+
{ mode: 'jwt', key: 'pinoox_ecosystem' }
|
|
454
|
+
// no provider, no source
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**When to use**
|
|
458
|
+
|
|
459
|
+
- The SPA only needs `mode` + `key` to store/send the token
|
|
460
|
+
- You do **not** want `provider` / `source` package ids visible in page HTML
|
|
461
|
+
- Strategy / login URLs are either defaults (`local`) or set in JS / via a **map** (form 4)
|
|
462
|
+
|
|
463
|
+
**Allowed whitelist names** (only these exist on the base):
|
|
464
|
+
|
|
465
|
+
| Field | Purpose |
|
|
466
|
+
| ----- | ------- |
|
|
467
|
+
| `mode` | How the token is stored / sent |
|
|
468
|
+
| `key` | Storage key (`localStorage` name) |
|
|
469
|
+
| `provider` | Auth-owning package |
|
|
470
|
+
| `source` | Transport auth source package |
|
|
471
|
+
|
|
472
|
+
Unknown names are ignored. You **cannot** whitelist `strategy` / `loginUrl` here — those are not base fields; use a **map** (below).
|
|
473
|
+
|
|
474
|
+
Another example — token key only (mode defaults to `jwt` in `createAuth` if missing, but publishing mode is safer):
|
|
475
|
+
|
|
476
|
+
```php
|
|
477
|
+
'client' => ['key'],
|
|
478
|
+
// → __PINOOX__.auth = { key: 'pinoox_ecosystem' }
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### 4) Map — base + extras (remote apps)
|
|
482
|
+
|
|
483
|
+
An **associative array** merges onto the full base. Use this for `strategy`, `loginUrl`, `baseUrl`, `endpoints`:
|
|
484
|
+
|
|
485
|
+
```php
|
|
486
|
+
// com_pinoox_shop — depends on Account
|
|
487
|
+
'auth' => [
|
|
488
|
+
'client' => [
|
|
489
|
+
'strategy' => 'remote',
|
|
490
|
+
'loginUrl' => '/account/login',
|
|
491
|
+
'baseUrl' => '/account/api/v1',
|
|
492
|
+
'endpoints' => [
|
|
493
|
+
'me' => 'auth/get',
|
|
494
|
+
'logout' => 'auth/logout',
|
|
495
|
+
],
|
|
496
|
+
],
|
|
497
|
+
],
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
// window.__PINOOX__.auth
|
|
502
|
+
{
|
|
503
|
+
mode: 'jwt',
|
|
504
|
+
key: 'pinoox_ecosystem', // inherited via transport from Account
|
|
505
|
+
provider: '…',
|
|
506
|
+
source: '…',
|
|
507
|
+
strategy: 'remote',
|
|
508
|
+
loginUrl: '/account/login',
|
|
509
|
+
baseUrl: '/account/api/v1',
|
|
510
|
+
endpoints: { me: 'auth/get', logout: 'auth/logout' },
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**List vs map (easy rule)**
|
|
515
|
+
|
|
516
|
+
| Shape | PHP | Meaning |
|
|
517
|
+
| ----- | --- | ------- |
|
|
518
|
+
| List | `['mode', 'key']` | Filter base → only these keys |
|
|
519
|
+
| Map | `['strategy' => 'remote', …]` | Keep **all** base keys, then merge extras |
|
|
520
|
+
|
|
521
|
+
```php
|
|
522
|
+
// ❌ Wrong — this is a list, so "strategy" is ignored (not a base field)
|
|
523
|
+
'client' => ['mode', 'key', 'strategy'],
|
|
524
|
+
|
|
525
|
+
// ✅ Right — map merges strategy onto the full base
|
|
526
|
+
'client' => [
|
|
527
|
+
'strategy' => 'remote',
|
|
528
|
+
'loginUrl' => '/account/login',
|
|
529
|
+
],
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Aliases for `auth.client`: `via`, `expose`, `bootstrap`.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## 7. App configuration reference
|
|
537
|
+
|
|
538
|
+
### Strategies
|
|
539
|
+
|
|
540
|
+
| Value | Meaning |
|
|
541
|
+
| ----- | ------- |
|
|
542
|
+
| `local` (default) | This app owns login |
|
|
543
|
+
| `remote` | Redirect to another app’s login |
|
|
544
|
+
|
|
545
|
+
Aliases: `provider` → `local`, `consumer` / `external` → `remote`.
|
|
546
|
+
|
|
547
|
+
### `baseUrl` + endpoints
|
|
548
|
+
|
|
549
|
+
Prefer a shared API prefix (here: Account as provider):
|
|
550
|
+
|
|
551
|
+
```php
|
|
552
|
+
'baseUrl' => '/account/api/v1',
|
|
553
|
+
'endpoints' => [
|
|
554
|
+
'me' => 'auth/get', // → /account/api/v1/auth/get
|
|
555
|
+
'logout' => 'auth/logout',
|
|
556
|
+
],
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Or absolute paths (no `baseUrl`):
|
|
560
|
+
|
|
561
|
+
```php
|
|
562
|
+
'endpoints' => [
|
|
563
|
+
'me' => '/account/api/v1/auth/get',
|
|
564
|
+
'logout' => '/account/api/v1/auth/logout',
|
|
565
|
+
],
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
For a dependent that validates the session on **its own** API (same shared key):
|
|
569
|
+
|
|
570
|
+
```php
|
|
571
|
+
// com_pinoox_crm with transport.user => platform or account
|
|
572
|
+
'baseUrl' => '/crm/api/v1',
|
|
573
|
+
'endpoints' => [
|
|
574
|
+
'me' => 'auth/get',
|
|
575
|
+
'logout' => 'auth/logout',
|
|
576
|
+
],
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Rules:
|
|
580
|
+
|
|
581
|
+
- `https://…` or path starting with `/` → used as-is
|
|
582
|
+
- relative + `baseUrl` → joined
|
|
583
|
+
- relative without `baseUrl` → left as-is
|
|
584
|
+
|
|
585
|
+
### Modes
|
|
586
|
+
|
|
587
|
+
| mode | Storage | Request |
|
|
588
|
+
| ---- | ------- | ------- |
|
|
589
|
+
| `jwt` | token under `auth.key` | `Authorization: Bearer …` |
|
|
590
|
+
| `cookie` / `session` | optional | cookies (`credentials: include`) |
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 8. Theme integration
|
|
595
|
+
|
|
596
|
+
### Login page (local / provider only)
|
|
597
|
+
|
|
598
|
+
```vue
|
|
599
|
+
<script setup>
|
|
600
|
+
import { http, useAuthStore } from '@/utils/auth'
|
|
601
|
+
import { useAuthRedirect } from '@pinooxhq/auth/vue'
|
|
602
|
+
|
|
603
|
+
const store = useAuthStore()
|
|
604
|
+
const { redirectBack } = useAuthRedirect()
|
|
605
|
+
|
|
606
|
+
const onLogin = async () => {
|
|
607
|
+
const { data } = await http.post('auth/login', { username, password })
|
|
608
|
+
const body = data?.data ?? data
|
|
609
|
+
store.login(body.token, body.user)
|
|
610
|
+
redirectBack()
|
|
611
|
+
}
|
|
612
|
+
</script>
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Router guard (any app)
|
|
616
|
+
|
|
617
|
+
```js
|
|
618
|
+
import { auth, useAuthStore } from '@/utils/auth'
|
|
619
|
+
|
|
620
|
+
export async function authGuard(to, _from, next) {
|
|
621
|
+
const store = useAuthStore()
|
|
622
|
+
store.syncTokenFromStorage()
|
|
623
|
+
|
|
624
|
+
if (!store.isAuth) {
|
|
625
|
+
await store.canUserAccess(true)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (to.meta.requiresAuth && !store.isAuth) {
|
|
629
|
+
auth.redirectToLogin()
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
next()
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
On remote apps (Shop / CRM), `auth.redirectToLogin()` goes to `loginUrl?redirect=<current path>`.
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## 9. HTTP, redirects, adapters
|
|
642
|
+
|
|
643
|
+
### HTTP
|
|
644
|
+
|
|
645
|
+
`createHttp({ auth, axios })` wires:
|
|
646
|
+
|
|
647
|
+
- `Authorization` from `auth.getAuthHeader()`
|
|
648
|
+
- `401` → `auth.notifyUnauthorized()`
|
|
649
|
+
- shared transport for `login` / `me` / `logout`
|
|
650
|
+
|
|
651
|
+
`baseURL` defaults to `__PINOOX__.url.API`.
|
|
652
|
+
|
|
653
|
+
### Redirects
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
auth.redirectToLogin() // remote → other app; local → loginUrl / endpoints.login
|
|
657
|
+
auth.redirectBack() // after login → safe ?redirect= (SITE origin aware for Vite HMR)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```js
|
|
661
|
+
import { useAuthRedirect } from '@pinooxhq/auth/vue'
|
|
662
|
+
const { redirectQuery, redirectBack, returnUrl } = useAuthRedirect()
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Vue / React / Svelte
|
|
666
|
+
|
|
667
|
+
```js
|
|
668
|
+
import { useAuth, useAuthRedirect, createPiniaAuthStore } from '@pinooxhq/auth/vue'
|
|
669
|
+
import { AuthProvider, useAuth } from '@pinooxhq/auth/react'
|
|
670
|
+
import { createAuthStore, createAuthRedirect } from '@pinooxhq/auth/svelte'
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## 10. API reference
|
|
676
|
+
|
|
677
|
+
```js
|
|
678
|
+
auth.login({ username, password }) // local only; remote → redirect
|
|
679
|
+
auth.me()
|
|
680
|
+
auth.logout()
|
|
681
|
+
auth.getToken() / auth.setToken(t) / auth.clearToken()
|
|
682
|
+
auth.getAuthHeader()
|
|
683
|
+
auth.redirectToLogin() / auth.redirectBack()
|
|
684
|
+
auth.getReturnUrl() / auth.getRedirectQuery()
|
|
685
|
+
auth.notifyUnauthorized()
|
|
686
|
+
|
|
687
|
+
http.get / http.post / …
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 11. Vite
|
|
693
|
+
|
|
694
|
+
[`@pinooxhq/vite-plugin`](https://www.npmjs.com/package/@pinooxhq/vite-plugin) — Twig shell + Vite entry so `__PINOOX__` (and `auth.client`) loads before the SPA.
|
|
695
|
+
|
|
696
|
+
```twig
|
|
697
|
+
{{ pinoox_bootstrap(bootstrap|default({}))|raw }}
|
|
698
|
+
{{ vite_tags() }}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## License
|
|
704
|
+
|
|
705
|
+
MIT
|