@jskit-ai/agent-docs 0.1.1
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/DISTR_AGENT.md +25 -0
- package/guide/agent/app-extras/assistant.md +636 -0
- package/guide/agent/app-extras/realtime.md +223 -0
- package/guide/agent/app-setup/a-more-interesting-shell.md +643 -0
- package/guide/agent/app-setup/authentication.md +948 -0
- package/guide/agent/app-setup/console.md +316 -0
- package/guide/agent/app-setup/database-layer.md +775 -0
- package/guide/agent/app-setup/initial-scaffolding.md +714 -0
- package/guide/agent/app-setup/multi-homing.md +655 -0
- package/guide/agent/app-setup/users.md +355 -0
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +983 -0
- package/guide/agent/generators/advanced-cruds.md +923 -0
- package/guide/agent/generators/crud-generators.md +556 -0
- package/guide/agent/generators/intro.md +63 -0
- package/guide/agent/generators/ui-generators.md +648 -0
- package/guide/agent/index.md +39 -0
- package/guide/human/app-extras/assistant.md +695 -0
- package/guide/human/app-extras/realtime.md +270 -0
- package/guide/human/app-setup/a-more-interesting-shell.md +734 -0
- package/guide/human/app-setup/authentication.md +963 -0
- package/guide/human/app-setup/console.md +352 -0
- package/guide/human/app-setup/database-layer.md +822 -0
- package/guide/human/app-setup/initial-scaffolding.md +738 -0
- package/guide/human/app-setup/multi-homing.md +795 -0
- package/guide/human/app-setup/users.md +404 -0
- package/guide/human/app-setup/working-with-the-jskit-cli.md +997 -0
- package/guide/human/generators/advanced-cruds.md +923 -0
- package/guide/human/generators/crud-generators.md +556 -0
- package/guide/human/generators/intro.md +109 -0
- package/guide/human/generators/ui-generators.md +665 -0
- package/guide/human/index.md +39 -0
- package/package.json +28 -0
- package/reference/autogen/KERNEL_MAP.md +536 -0
- package/reference/autogen/README.md +44 -0
- package/reference/autogen/packages/agent-docs.md +13 -0
- package/reference/autogen/packages/assistant-core.md +310 -0
- package/reference/autogen/packages/assistant-runtime.md +219 -0
- package/reference/autogen/packages/assistant.md +73 -0
- package/reference/autogen/packages/auth-core.md +352 -0
- package/reference/autogen/packages/auth-provider-supabase-core.md +223 -0
- package/reference/autogen/packages/auth-web.md +267 -0
- package/reference/autogen/packages/console-core.md +116 -0
- package/reference/autogen/packages/console-web.md +37 -0
- package/reference/autogen/packages/crud-core.md +283 -0
- package/reference/autogen/packages/crud-server-generator.md +220 -0
- package/reference/autogen/packages/crud-ui-generator.md +154 -0
- package/reference/autogen/packages/database-runtime-mysql.md +61 -0
- package/reference/autogen/packages/database-runtime-postgres.md +39 -0
- package/reference/autogen/packages/database-runtime.md +216 -0
- package/reference/autogen/packages/http-runtime.md +213 -0
- package/reference/autogen/packages/kernel.md +1350 -0
- package/reference/autogen/packages/realtime.md +95 -0
- package/reference/autogen/packages/shell-web.md +349 -0
- package/reference/autogen/packages/storage-runtime.md +39 -0
- package/reference/autogen/packages/ui-generator.md +101 -0
- package/reference/autogen/packages/uploads-image-web.md +76 -0
- package/reference/autogen/packages/uploads-runtime.md +85 -0
- package/reference/autogen/packages/users-core.md +307 -0
- package/reference/autogen/packages/users-web.md +473 -0
- package/reference/autogen/packages/workspaces-core.md +415 -0
- package/reference/autogen/packages/workspaces-web.md +372 -0
- package/reference/autogen/tooling/config-eslint.md +52 -0
- package/reference/autogen/tooling/create-app.md +194 -0
- package/reference/autogen/tooling/jskit-catalog.md +27 -0
- package/reference/autogen/tooling/jskit-cli.md +624 -0
- package/reference/autogen/tooling/test-support.md +27 -0
- package/reference/autogen/tooling/testUtils.md +31 -0
- package/templates/APP_BLUEPRINT.md +57 -0
- package/workflow/app-state.md +33 -0
- package/workflow/bootstrap.md +24 -0
- package/workflow/feature-delivery.md +21 -0
- package/workflow/review.md +22 -0
- package/workflow/scoping.md +26 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
<!-- Generated by `npm run agent-docs:build` from `docs/guide/app-setup/authentication.md`. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# Authentication
|
|
4
|
+
|
|
5
|
+
At the end of the previous chapter, the app had a real shell, but it still did not know how to sign users in. In this chapter, we connect the app to Supabase Auth, add the stock JSKIT login and sign-out routes, and inspect the files that make authentication visible in the shell.
|
|
6
|
+
|
|
7
|
+
This chapter still does **not** add the database-backed users layer. That is intentional. It lets us see the authentication pieces clearly before account pages, profile storage, and workspace membership are added later.
|
|
8
|
+
|
|
9
|
+
To get back to the same starting point as the end of the previous chapter, run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @jskit-ai/create-app exampleapp --tenancy-mode none
|
|
13
|
+
cd exampleapp
|
|
14
|
+
npm install
|
|
15
|
+
|
|
16
|
+
npx jskit add package shell-web
|
|
17
|
+
npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If you are already continuing from the previous chapter, you are already in the right place and can skip that setup.
|
|
21
|
+
|
|
22
|
+
**Supabase: Create The Project First**
|
|
23
|
+
|
|
24
|
+
Before installing the auth packages, create a Supabase project and collect the three values JSKIT needs.
|
|
25
|
+
|
|
26
|
+
1. In the Supabase dashboard, create a new project in your organization and wait for it to finish provisioning.
|
|
27
|
+
2. Open `Project Settings -> API`. Copy:
|
|
28
|
+
- the **Project URL**
|
|
29
|
+
- the **publishable** key
|
|
30
|
+
3. Open `Authentication -> URL Configuration`.
|
|
31
|
+
4. Set **Site URL** to:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
http://localhost:5173
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
5. Add this local redirect URL:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
http://localhost:5173/auth/login
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
JSKIT uses the project URL as `AUTH_SUPABASE_URL`, the publishable key as `AUTH_SUPABASE_PUBLISHABLE_KEY`, and the browser address as `APP_PUBLIC_URL`.
|
|
44
|
+
|
|
45
|
+
Use the **publishable** key here, not the secret key and not a service-role key. If you later run the app on a different host or port, update both Supabase's URL settings and `APP_PUBLIC_URL` to match the real browser URL exactly.
|
|
46
|
+
|
|
47
|
+
## Installing the auth layer
|
|
48
|
+
|
|
49
|
+
From inside `exampleapp`, run:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx jskit add package auth-provider-supabase-core \
|
|
53
|
+
--auth-supabase-url "https://YOUR-PROJECT.supabase.co" \
|
|
54
|
+
--auth-supabase-publishable-key "sb_publishable_..." \
|
|
55
|
+
--app-public-url "http://localhost:5173"
|
|
56
|
+
|
|
57
|
+
npx jskit add bundle auth-base
|
|
58
|
+
npm install
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The first command is the provider step. It tells JSKIT which authentication backend to use and writes the project-specific values into the app.
|
|
62
|
+
|
|
63
|
+
The second command adds the web auth layer. `auth-base` is a small bundle, not a black box: in practice it adds `auth-core` and `auth-web`, which together provide the auth routes, the auth surface, and the stock login and sign-out screens.
|
|
64
|
+
|
|
65
|
+
The final `npm install` matters for the same reason it did in the shell chapter: `jskit add` rewrites the scaffold and updates `package.json`, but `npm install` is what actually downloads the newly referenced runtime packages.
|
|
66
|
+
|
|
67
|
+
## Running it
|
|
68
|
+
|
|
69
|
+
Authentication needs both the browser-facing dev server and the backend runtime, so start both:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run dev
|
|
73
|
+
npm run server
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then open `http://localhost:5173/auth/login` in the browser.
|
|
77
|
+
|
|
78
|
+
The login page is now real, and it already contains several different auth modes behind the same card.
|
|
79
|
+
|
|
80
|
+
- **Sign in** is the normal email-and-password flow.
|
|
81
|
+
- **Register** creates a new Supabase auth user.
|
|
82
|
+
- **Forgot password?** requests a password reset email.
|
|
83
|
+
- **Use one-time code** switches to an email OTP login flow.
|
|
84
|
+
- **Remember this account on this device** stores a small local hint in browser storage so the next visit can greet the last-used account and let the user keep that email preselected.
|
|
85
|
+
|
|
86
|
+
If you go back to `http://localhost:5173/home`, the shell also has a small auth widget in the top-right corner. When you are signed out it shows a guest state and a menu entry that leads to `/auth/login`. After you sign in, the same placement changes to a sign-out menu.
|
|
87
|
+
|
|
88
|
+
## Reading the screen carefully
|
|
89
|
+
|
|
90
|
+
The login card is not a single form. It is a small state machine that switches between several modes.
|
|
91
|
+
|
|
92
|
+
### Normal sign-in mode
|
|
93
|
+
|
|
94
|
+
This is the first screen you see.
|
|
95
|
+
|
|
96
|
+
- The `Email` field is validated as an email address.
|
|
97
|
+
- The `Password` field is validated with the password rules from the shared auth command schema.
|
|
98
|
+
- The eye icon only changes whether the password is visible in the browser.
|
|
99
|
+
- The main `Sign in` button posts to `POST /api/login`.
|
|
100
|
+
- If sign-in succeeds, JSKIT refreshes the current session and redirects back to the requested return path.
|
|
101
|
+
|
|
102
|
+
The small links under the password field are not decoration.
|
|
103
|
+
|
|
104
|
+
- `Forgot password?` switches the card into password-reset-request mode.
|
|
105
|
+
- `Use one-time code` switches the card into OTP mode.
|
|
106
|
+
|
|
107
|
+
### Register mode
|
|
108
|
+
|
|
109
|
+
When the user presses `Register`, the form changes in three important ways.
|
|
110
|
+
|
|
111
|
+
- The title changes from `Welcome` to `Create your account`.
|
|
112
|
+
- A `Confirm password` field appears.
|
|
113
|
+
- The main submit button changes from `Sign in` to `Register`.
|
|
114
|
+
|
|
115
|
+
Pressing `Register` sends a sign-up request to Supabase through `POST /api/register`. JSKIT also sends a starter `display_name` value derived from the email prefix, so a user registering as `alice@example.com` starts with a display name like `alice`.
|
|
116
|
+
|
|
117
|
+
What happens next depends on Supabase's email-confirmation settings.
|
|
118
|
+
|
|
119
|
+
- If email confirmation is required, Supabase creates the user immediately but does not return a live session. The card then switches to a dedicated confirmation state.
|
|
120
|
+
- If email confirmation is not required, the user is registered and signed in immediately.
|
|
121
|
+
|
|
122
|
+
### Email-confirmation state
|
|
123
|
+
|
|
124
|
+
If Supabase requires confirmation, the screen changes again.
|
|
125
|
+
|
|
126
|
+
- The mode-switch buttons disappear.
|
|
127
|
+
- The card shows a confirmation message instead of the email/password fields.
|
|
128
|
+
- `Go to main screen` returns to the app.
|
|
129
|
+
- `Resend confirmation email` calls `POST /api/register/confirmation/resend`.
|
|
130
|
+
- `Back to sign in` returns to the normal login mode.
|
|
131
|
+
|
|
132
|
+
This matters because a junior developer might otherwise assume registration failed. In reality, the user usually already exists in Supabase at this point; they just do not have an active session yet.
|
|
133
|
+
|
|
134
|
+
### One-time-code mode
|
|
135
|
+
|
|
136
|
+
`Use one-time code` changes the form again.
|
|
137
|
+
|
|
138
|
+
- The password field disappears.
|
|
139
|
+
- A `One-time code` field appears.
|
|
140
|
+
- A secondary button called `Send one-time code` appears.
|
|
141
|
+
- The main button changes to `Verify code`.
|
|
142
|
+
|
|
143
|
+
Those two buttons do different jobs.
|
|
144
|
+
|
|
145
|
+
- `Send one-time code` requests the email through `POST /api/login/otp/request`.
|
|
146
|
+
- `Verify code` submits the code through `POST /api/login/otp/verify`.
|
|
147
|
+
|
|
148
|
+
JSKIT asks Supabase to send OTP login emails only for existing users. In other words, this flow is for signing in, not for silently creating a new account.
|
|
149
|
+
|
|
150
|
+
### Password-reset-request mode
|
|
151
|
+
|
|
152
|
+
`Forgot password?` does not immediately let the user type a new password. It switches to a reset-request mode.
|
|
153
|
+
|
|
154
|
+
- The password field disappears.
|
|
155
|
+
- The main button changes to `Send reset instructions`.
|
|
156
|
+
- Submitting this screen calls `POST /api/password/forgot`.
|
|
157
|
+
|
|
158
|
+
That endpoint asks Supabase to send a password reset email. In this chapter's scaffold, this mode is only the **request** step.
|
|
159
|
+
|
|
160
|
+
The backend already supports the later recovery endpoints too, but the chapter's simple app does **not** yet scaffold a dedicated app-owned `reset-password` page. So at this stage, the guide should be read as: the screen can request recovery emails now, while the full browser-side password-reset completion UI is still something you would add explicitly in the app.
|
|
161
|
+
|
|
162
|
+
### Remembered account behavior
|
|
163
|
+
|
|
164
|
+
The checkbox `Remember this account on this device` is also worth understanding.
|
|
165
|
+
|
|
166
|
+
If it stays checked and the user successfully signs in, registers, or verifies an OTP code, JSKIT stores a small browser-local hint with:
|
|
167
|
+
|
|
168
|
+
- the normalized email address
|
|
169
|
+
- a masked version of the email
|
|
170
|
+
- a display name
|
|
171
|
+
- a `lastUsedAt` timestamp
|
|
172
|
+
|
|
173
|
+
On the next visit, the card can show a `Welcome back, ...` panel and a `Use another account` button. This is only a browser convenience feature. It is not a second authentication factor, and it is not a server-side session store.
|
|
174
|
+
|
|
175
|
+
### OAuth buttons
|
|
176
|
+
|
|
177
|
+
The screen is also ready to show OAuth provider buttons, but only if providers are configured.
|
|
178
|
+
|
|
179
|
+
Right now the chapter's `config.server.js` keeps this empty:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
config.auth = {
|
|
183
|
+
oauth: {
|
|
184
|
+
providers: [],
|
|
185
|
+
defaultProvider: ""
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
That is why the login card in this chapter does not show buttons like `Continue with Google`.
|
|
191
|
+
|
|
192
|
+
To turn on Google later, there are two separate setup steps.
|
|
193
|
+
|
|
194
|
+
First, configure Google and Supabase:
|
|
195
|
+
|
|
196
|
+
1. In Google Auth Platform, create a **Web application** OAuth client.
|
|
197
|
+
2. Add your browser URLs as **Authorized JavaScript origins**.
|
|
198
|
+
3. In Supabase, open the Google provider settings and copy the provider callback URL shown there.
|
|
199
|
+
4. Add that Supabase callback URL as an **Authorized redirect URI** on the Google OAuth client.
|
|
200
|
+
5. Back in Supabase, paste the Google **Client ID** and **Client Secret** into the Google provider settings and enable the provider.
|
|
201
|
+
6. Make sure Supabase's **Site URL** and **Redirect URLs** still match the real browser URL your app uses.
|
|
202
|
+
|
|
203
|
+
Then tell JSKIT to expose the provider in the login UI:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
config.auth = {
|
|
207
|
+
oauth: {
|
|
208
|
+
providers: ["google"],
|
|
209
|
+
defaultProvider: "google"
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`providers` controls which OAuth buttons the stock login screen is allowed to render. `defaultProvider` tells JSKIT which provider to prefer when it needs a default choice. If the provider is configured in Supabase but missing from this list, the button still does not appear in the JSKIT login screen.
|
|
215
|
+
|
|
216
|
+
**Important: What Works Without A Database**
|
|
217
|
+
|
|
218
|
+
Authentication is already real in this chapter because Supabase is still the source of truth for the important auth data:
|
|
219
|
+
|
|
220
|
+
- the real auth user record
|
|
221
|
+
- the password hash and password-reset state
|
|
222
|
+
- the OTP and OAuth flows
|
|
223
|
+
- the access and refresh tokens that JSKIT writes into cookies
|
|
224
|
+
|
|
225
|
+
What is missing is JSKIT's own database-backed users layer. In no-database mode, the auth provider switches to **standalone in-memory fallbacks** for the app-side data it normally mirrors into JSKIT tables.
|
|
226
|
+
|
|
227
|
+
Concretely, that means:
|
|
228
|
+
|
|
229
|
+
- JSKIT still creates a local profile mirror for each authenticated Supabase user.
|
|
230
|
+
- But that mirror lives only in the Node process, not in a database table.
|
|
231
|
+
- That temporary mirror stores only a small app-side profile shape:
|
|
232
|
+
- `id`
|
|
233
|
+
- `authProvider`
|
|
234
|
+
- `authProviderUserSid`
|
|
235
|
+
- `email`
|
|
236
|
+
- `displayName`
|
|
237
|
+
- JSKIT also keeps a tiny in-memory user-settings record for auth-related flags such as:
|
|
238
|
+
- `passwordSignInEnabled`
|
|
239
|
+
- `passwordSetupRequired`
|
|
240
|
+
|
|
241
|
+
So the behavior is:
|
|
242
|
+
|
|
243
|
+
- register a user -> the real user is created in Supabase
|
|
244
|
+
- sign in -> Supabase still verifies credentials and returns the real session
|
|
245
|
+
- JSKIT then mirrors just enough profile data into memory so the app can work
|
|
246
|
+
- restart the local server -> that JSKIT-side mirror and those fallback settings are cleared
|
|
247
|
+
|
|
248
|
+
The browser session is a different thing. In the normal case, a server restart does **not** log the browser out. The browser still has the auth cookies, so on the next request JSKIT can read those cookies, validate or refresh the Supabase session, and rebuild the temporary mirror.
|
|
249
|
+
|
|
250
|
+
That last point is the important difference. A restart does **not** delete the Supabase user. It does **not** erase the real password. It does **not** erase the real auth session in Supabase itself. What it clears is only the app's temporary in-memory mirror. On the next authenticated request, JSKIT rebuilds that mirror from the Supabase user or token claims.
|
|
251
|
+
|
|
252
|
+
So without a database, you still get:
|
|
253
|
+
|
|
254
|
+
- real login
|
|
255
|
+
- real logout
|
|
256
|
+
- real registration
|
|
257
|
+
- real password reset requests
|
|
258
|
+
- real OTP and OAuth flows
|
|
259
|
+
- real session cookies
|
|
260
|
+
|
|
261
|
+
But you do **not** get:
|
|
262
|
+
|
|
263
|
+
- persistent JSKIT-side user rows
|
|
264
|
+
- persistent JSKIT-side user settings
|
|
265
|
+
- account/profile persistence beyond what Supabase itself stores
|
|
266
|
+
- workspace membership, user preferences, or the later users/workspaces data model
|
|
267
|
+
|
|
268
|
+
Later, when the guide installs `users-web`, auth stops using the standalone fallback and starts resolving `users.profile.sync.service` from `users-core` instead.
|
|
269
|
+
|
|
270
|
+
That service exposes three main functions:
|
|
271
|
+
|
|
272
|
+
- `findByIdentity(...)` to look up the JSKIT-side user for an auth identity
|
|
273
|
+
- `upsertByIdentity(...)` to create or update the JSKIT-side user record directly
|
|
274
|
+
- `syncIdentityProfile(...)` to run the normal auth-driven synchronization flow
|
|
275
|
+
|
|
276
|
+
`syncIdentityProfile(...)` is the one auth actually relies on. It is the method that synchronizes the JSKIT-side user record, ensures the related settings row exists through `ensureForUserId(...)`, and then runs any registered post-sync lifecycle contributors.
|
|
277
|
+
|
|
278
|
+
So this chapter gives you real authentication, but only a temporary app-side user mirror. The full persistent JSKIT user model comes later with the database and users layers.
|
|
279
|
+
|
|
280
|
+
## Using auth in your own app
|
|
281
|
+
|
|
282
|
+
The most important thing this chapter gives you is not just a login page. It gives you three real app-building tools:
|
|
283
|
+
|
|
284
|
+
- a route-level auth guard
|
|
285
|
+
- auth-aware placement predicates
|
|
286
|
+
- a client-side auth composable you can read in your own components
|
|
287
|
+
|
|
288
|
+
Those are different tools, and they do different jobs.
|
|
289
|
+
|
|
290
|
+
- A route guard protects a URL.
|
|
291
|
+
- A placement predicate controls whether a menu entry or widget is visible.
|
|
292
|
+
- The auth composable lets your component react to the current session state.
|
|
293
|
+
|
|
294
|
+
That separation matters. Protecting a route does **not** automatically hide a menu entry, and hiding a menu entry does **not** protect a route.
|
|
295
|
+
|
|
296
|
+
### Start with a normal public page
|
|
297
|
+
|
|
298
|
+
Generate a simple page under the public `home` surface:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
npx jskit generate ui-generator page home/reports/index.vue --name "Reports"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
At this point the page is still public, because `home` is a public surface. JSKIT creates:
|
|
305
|
+
|
|
306
|
+
- `src/pages/home/reports/index.vue`
|
|
307
|
+
- a new menu placement in `src/placement.js`
|
|
308
|
+
|
|
309
|
+
The placement entry is just a normal shell link:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
addPlacement({
|
|
313
|
+
id: "ui-generator.page.home.reports.link",
|
|
314
|
+
target: "shell-layout:primary-menu",
|
|
315
|
+
surfaces: ["home"],
|
|
316
|
+
order: 155,
|
|
317
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
318
|
+
props: {
|
|
319
|
+
label: "Reports",
|
|
320
|
+
surface: "home",
|
|
321
|
+
scopedSuffix: "/reports",
|
|
322
|
+
unscopedSuffix: "/reports"
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
And the page file itself is a normal page scaffold:
|
|
328
|
+
|
|
329
|
+
```vue
|
|
330
|
+
<template>
|
|
331
|
+
<section class="pa-4">
|
|
332
|
+
<h1 class="text-h5 mb-2">Reports</h1>
|
|
333
|
+
<p class="text-body-2 text-medium-emphasis">Replace this scaffold with your page implementation.</p>
|
|
334
|
+
</section>
|
|
335
|
+
</template>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
So immediately after generation:
|
|
339
|
+
|
|
340
|
+
- the `Reports` menu entry is visible to everyone
|
|
341
|
+
- `/home/reports` is reachable by everyone
|
|
342
|
+
|
|
343
|
+
### Gate the page behind login
|
|
344
|
+
|
|
345
|
+
To make the route require login, add a route guard block to `src/pages/home/reports/index.vue`:
|
|
346
|
+
|
|
347
|
+
```vue
|
|
348
|
+
<route lang="json">
|
|
349
|
+
{
|
|
350
|
+
"meta": {
|
|
351
|
+
"guard": {
|
|
352
|
+
"policy": "authenticated"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
</route>
|
|
357
|
+
|
|
358
|
+
<template>
|
|
359
|
+
<section class="pa-4">
|
|
360
|
+
<h1 class="text-h5 mb-2">Reports</h1>
|
|
361
|
+
<p class="text-body-2 text-medium-emphasis">Replace this scaffold with your page implementation.</p>
|
|
362
|
+
</section>
|
|
363
|
+
</template>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
That one change protects the route itself. If a signed-out user tries to visit `/home/reports`, the auth guard runtime redirects them to the login route instead of letting the page render.
|
|
367
|
+
|
|
368
|
+
The redirect also keeps the requested target. In practice the browser ends up on a login URL shaped like this:
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
/auth/login?returnTo=%2Fhome%2Freports
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
So after login, JSKIT can send the user back to the page they originally asked for.
|
|
375
|
+
|
|
376
|
+
### Hide the menu entry when signed out
|
|
377
|
+
|
|
378
|
+
The route is now protected, but the drawer link is still visible. That is expected. Route protection and shell visibility are separate concerns.
|
|
379
|
+
|
|
380
|
+
To hide the `Reports` menu entry until the user is logged in, update the placement entry in `src/placement.js`:
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
addPlacement({
|
|
384
|
+
id: "ui-generator.page.home.reports.link",
|
|
385
|
+
target: "shell-layout:primary-menu",
|
|
386
|
+
surfaces: ["home"],
|
|
387
|
+
order: 155,
|
|
388
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
389
|
+
props: {
|
|
390
|
+
label: "Reports",
|
|
391
|
+
surface: "home",
|
|
392
|
+
scopedSuffix: "/reports",
|
|
393
|
+
unscopedSuffix: "/reports"
|
|
394
|
+
},
|
|
395
|
+
// Added: only show this menu entry when the current auth context is authenticated.
|
|
396
|
+
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The only new part is the `when(...)` line. That predicate is evaluated by the shell placement runtime using the auth context that `auth-web` injects from `/api/session`.
|
|
401
|
+
|
|
402
|
+
So the behavior now becomes:
|
|
403
|
+
|
|
404
|
+
- signed out:
|
|
405
|
+
- the `Reports` drawer entry disappears
|
|
406
|
+
- visiting `/home/reports` manually still redirects to `/auth/login`
|
|
407
|
+
- signed in:
|
|
408
|
+
- the `Reports` drawer entry appears
|
|
409
|
+
- `/home/reports` renders normally
|
|
410
|
+
|
|
411
|
+
This is the most important pattern to understand: use the guard to protect the route, and use the placement `when(...)` function to control whether the shell exposes a link to it.
|
|
412
|
+
|
|
413
|
+
### Read auth state in your own page code
|
|
414
|
+
|
|
415
|
+
Sometimes you do not want to redirect or hide a menu entry. You just want the page to react differently when a user is logged in.
|
|
416
|
+
|
|
417
|
+
For that, use `useAuthStore()` from `auth-web`. By this point in the guide the scaffold already has Pinia installed from day 0, and `shell-web` has already shown the same store-facing pattern for shell UI state. `auth-web` adds the auth version of that pattern: a Pinia store facade over the underlying auth runtime, so normal Vue code can read the session state without manually wiring subscriptions.
|
|
418
|
+
|
|
419
|
+
Here is a small example that changes `src/pages/home/index.vue` so it shows a success message when the session is authenticated:
|
|
420
|
+
|
|
421
|
+
```vue
|
|
422
|
+
<script setup>
|
|
423
|
+
import { useAuthStore } from "@jskit-ai/auth-web/client";
|
|
424
|
+
|
|
425
|
+
const auth = useAuthStore();
|
|
426
|
+
</script>
|
|
427
|
+
|
|
428
|
+
<template>
|
|
429
|
+
<section class="pa-4">
|
|
430
|
+
<v-alert v-if="auth.authenticated" type="success" variant="tonal" class="mb-4">
|
|
431
|
+
You are logged in!
|
|
432
|
+
</v-alert>
|
|
433
|
+
|
|
434
|
+
<h1 class="text-h5 mb-2">Home</h1>
|
|
435
|
+
<p class="text-body-2 text-medium-emphasis">Replace this scaffold with your page implementation.</p>
|
|
436
|
+
</section>
|
|
437
|
+
</template>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The important thing about that snippet is how little it needs to know. `auth.authenticated` is already reactive through Pinia, so the banner updates automatically when the session changes.
|
|
441
|
+
|
|
442
|
+
`useAuthStore()` also gives you the rest of the surfaced auth state and the lower-level runtime methods when you need them:
|
|
443
|
+
|
|
444
|
+
- `authState`
|
|
445
|
+
- `authenticated`
|
|
446
|
+
- `username`
|
|
447
|
+
- `oauthProviders`
|
|
448
|
+
- `oauthDefaultProvider`
|
|
449
|
+
- `initialize()`
|
|
450
|
+
- `refresh()`
|
|
451
|
+
- `getState()`
|
|
452
|
+
- `subscribe()`
|
|
453
|
+
- `runtime`
|
|
454
|
+
|
|
455
|
+
If you need one of those methods, keep the whole auth object instead of only destructuring a single ref:
|
|
456
|
+
|
|
457
|
+
```vue
|
|
458
|
+
<script setup>
|
|
459
|
+
import { useAuthStore } from "@jskit-ai/auth-web/client";
|
|
460
|
+
|
|
461
|
+
const auth = useAuthStore();
|
|
462
|
+
|
|
463
|
+
async function refreshSession() {
|
|
464
|
+
await auth.refresh();
|
|
465
|
+
console.log(auth.getState());
|
|
466
|
+
}
|
|
467
|
+
</script>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
So this is not just useful for a demo banner. It is the same mechanism you would use for:
|
|
471
|
+
|
|
472
|
+
- guest vs authenticated copy
|
|
473
|
+
- showing a call-to-action only for signed-out users
|
|
474
|
+
- enabling a tool panel only for authenticated users
|
|
475
|
+
- rendering a user-specific welcome message
|
|
476
|
+
|
|
477
|
+
### The three auth tools, side by side
|
|
478
|
+
|
|
479
|
+
By this point the surfaced auth API should be clearer:
|
|
480
|
+
|
|
481
|
+
- route file meta:
|
|
482
|
+
- use `"policy": "authenticated"` when the page itself must be protected
|
|
483
|
+
- placement entry:
|
|
484
|
+
- use `when: ({ auth }) => Boolean(auth?.authenticated)` when shell UI should only appear for signed-in users
|
|
485
|
+
- component code:
|
|
486
|
+
- use `useAuthStore()` when the page needs to react to auth state directly
|
|
487
|
+
|
|
488
|
+
That is the real development payoff of this chapter. The login system is not just a screen. It gives the app a reusable auth state model that routing, shell placements, and component code can all use.
|
|
489
|
+
|
|
490
|
+
At this point the guide has shown three distinct layers of client state:
|
|
491
|
+
|
|
492
|
+
- the scaffold installs Pinia but does not expose any package stores yet
|
|
493
|
+
- `shell-web` adds shell-facing stores such as `useShellLayoutStore()`
|
|
494
|
+
- `auth-web` adds `useAuthStore()` for authentication state
|
|
495
|
+
|
|
496
|
+
That progression is intentional. Packages keep their operational runtimes internally, but the app-facing shared state they surface to Vue code is now store-based.
|
|
497
|
+
|
|
498
|
+
## What `auth-base` adds to the app
|
|
499
|
+
|
|
500
|
+
The interesting part of this chapter is that authentication appears in several different layers at once: environment config, public routing config, shell placements, and app-owned view wrappers.
|
|
501
|
+
|
|
502
|
+
The first new place to inspect is `package.json`:
|
|
503
|
+
|
|
504
|
+
```json
|
|
505
|
+
{
|
|
506
|
+
"scripts": {
|
|
507
|
+
"server:auth": "SERVER_SURFACE=auth node ./bin/server.js",
|
|
508
|
+
"dev:auth": "VITE_SURFACE=auth vite",
|
|
509
|
+
"build:auth": "VITE_SURFACE=auth vite build"
|
|
510
|
+
},
|
|
511
|
+
"dependencies": {
|
|
512
|
+
"@jskit-ai/auth-core": "0.x",
|
|
513
|
+
"@jskit-ai/auth-provider-supabase-core": "0.x",
|
|
514
|
+
"@jskit-ai/auth-web": "0.x"
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Three things are worth noticing immediately.
|
|
520
|
+
|
|
521
|
+
- `auth-provider-supabase-core` is the provider-specific runtime.
|
|
522
|
+
- `auth-web` is the part that adds the web routes and the default auth UI.
|
|
523
|
+
- there is now an `auth` surface-specific dev/build script family, just as `home` already had.
|
|
524
|
+
|
|
525
|
+
The provider command also writes a new `.env` file:
|
|
526
|
+
|
|
527
|
+
```dotenv
|
|
528
|
+
AUTH_PROVIDER=supabase
|
|
529
|
+
AUTH_SUPABASE_URL=https://YOUR-PROJECT.supabase.co
|
|
530
|
+
AUTH_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
|
|
531
|
+
APP_PUBLIC_URL=http://localhost:5173
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
This is the bridge between the scaffold and your real Supabase project. `APP_PUBLIC_URL` matters because auth emails and callback flows need to know which browser URL they should return to.
|
|
535
|
+
|
|
536
|
+
Public routing config changes too. `config/public.js` now has a second surface:
|
|
537
|
+
|
|
538
|
+
```js
|
|
539
|
+
config.surfaceDefinitions.auth = {
|
|
540
|
+
id: "auth",
|
|
541
|
+
label: "Auth",
|
|
542
|
+
pagesRoot: "auth",
|
|
543
|
+
enabled: true,
|
|
544
|
+
requiresAuth: false,
|
|
545
|
+
requiresWorkspace: false,
|
|
546
|
+
origin: ""
|
|
547
|
+
};
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
That `requiresAuth: false` line is important. The auth surface must stay public, otherwise users would need to be logged in before they could reach the login page.
|
|
551
|
+
|
|
552
|
+
`config/server.js` also gets an auth stub:
|
|
553
|
+
|
|
554
|
+
```js
|
|
555
|
+
config.auth = {
|
|
556
|
+
oauth: {
|
|
557
|
+
providers: [],
|
|
558
|
+
defaultProvider: ""
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
That small block explains a lot of the default login screen. The stock UI is ready for OAuth providers such as Google, but right now the provider list is empty, so the page only shows the email/password and one-time-code flows. Later, if you enable a provider in Supabase and list it here, the same login screen can expose that button too.
|
|
564
|
+
|
|
565
|
+
The auth routes themselves are app-owned wrappers around the module-supplied default views. `src/pages/auth/login.vue` looks like this:
|
|
566
|
+
|
|
567
|
+
```vue
|
|
568
|
+
<route lang="json">
|
|
569
|
+
{
|
|
570
|
+
"meta": {
|
|
571
|
+
"guard": {
|
|
572
|
+
"policy": "public"
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
</route>
|
|
577
|
+
|
|
578
|
+
<script setup>
|
|
579
|
+
import DefaultLoginView from "@jskit-ai/auth-web/client/views/DefaultLoginView";
|
|
580
|
+
</script>
|
|
581
|
+
|
|
582
|
+
<template>
|
|
583
|
+
<DefaultLoginView />
|
|
584
|
+
</template>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
There are two important ideas in that small file.
|
|
588
|
+
|
|
589
|
+
- The route is explicitly public.
|
|
590
|
+
- The app owns the page wrapper even though the actual stock login form comes from the installed module.
|
|
591
|
+
|
|
592
|
+
The `meta.guard` block is ordinary route metadata, not a special auth-only API. The file-based router plugin turns `<route lang="json">` into a normal Vue Router `meta` object first, and then JSKIT reads `meta.guard.policy` during navigation. That same mechanism is used elsewhere in the app too.
|
|
593
|
+
|
|
594
|
+
That pattern gives you a clean customization seam later. The module supplies a working default, but the app still owns the route file and can replace or wrap the view if needed.
|
|
595
|
+
|
|
596
|
+
Authentication also becomes visible in the shell through `src/placement.js`:
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
addPlacement({
|
|
600
|
+
id: "auth.profile.widget",
|
|
601
|
+
target: "shell-layout:top-right",
|
|
602
|
+
surfaces: ["*"],
|
|
603
|
+
order: 1000,
|
|
604
|
+
componentToken: "auth.web.profile.widget"
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
addPlacement({
|
|
608
|
+
id: "auth.profile.menu.sign-in",
|
|
609
|
+
target: "auth-profile-menu:primary-menu",
|
|
610
|
+
surfaces: ["*"],
|
|
611
|
+
order: 200,
|
|
612
|
+
componentToken: "auth.web.profile.menu.link-item",
|
|
613
|
+
props: {
|
|
614
|
+
label: "Sign in",
|
|
615
|
+
to: "/auth/login"
|
|
616
|
+
},
|
|
617
|
+
when: ({ auth }) => !Boolean(auth?.authenticated)
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
addPlacement({
|
|
621
|
+
id: "auth.profile.menu.sign-out",
|
|
622
|
+
target: "auth-profile-menu:primary-menu",
|
|
623
|
+
surfaces: ["*"],
|
|
624
|
+
order: 1000,
|
|
625
|
+
componentToken: "auth.web.profile.menu.link-item",
|
|
626
|
+
props: {
|
|
627
|
+
label: "Sign out",
|
|
628
|
+
to: "/auth/signout"
|
|
629
|
+
},
|
|
630
|
+
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
This is the shell placement system from the previous chapter doing real work again. `auth-web` does not hard-code a permanent top-right button into `ShellLayout.vue`. Instead, it contributes a widget and two menu entries into named outlets, and those entries react to the current auth state.
|
|
635
|
+
|
|
636
|
+
So the auth story in this chapter is spread across clear responsibilities:
|
|
637
|
+
|
|
638
|
+
- `.env` tells the provider runtime which Supabase project to talk to
|
|
639
|
+
- `config/public.js` declares an `auth` surface
|
|
640
|
+
- `config/server.js` exposes app-owned OAuth visibility settings
|
|
641
|
+
- `src/pages/auth/*` gives the app real public auth routes
|
|
642
|
+
- `src/placement.js` makes auth visible in the shell
|
|
643
|
+
|
|
644
|
+
That is a very JSKIT-style pattern. The installed package brings the runtime behavior, but the app still owns the important seams where routing and UI get attached.
|
|
645
|
+
|
|
646
|
+
## Under the hood
|
|
647
|
+
|
|
648
|
+
### The runtime behind `useAuthStore()`
|
|
649
|
+
|
|
650
|
+
`useAuthStore()` is the app-facing Pinia layer, but it is not inventing a second auth system. It is a store facade over the lower-level auth guard runtime that `auth-web` boots on startup.
|
|
651
|
+
|
|
652
|
+
That lower-level runtime already has a small, concrete contract:
|
|
653
|
+
|
|
654
|
+
- `initialize()`
|
|
655
|
+
- `refresh()`
|
|
656
|
+
- `getState()`
|
|
657
|
+
- `subscribe()`
|
|
658
|
+
|
|
659
|
+
`auth-web` initializes that runtime once, binds it into the Pinia auth store, and then exposes `useAuthStore()` as the normal component-facing API. That is why the main example earlier could stay so small.
|
|
660
|
+
|
|
661
|
+
If you strip the composable away and write the same `You are logged in!` example directly against the runtime, it looks like this:
|
|
662
|
+
|
|
663
|
+
```vue
|
|
664
|
+
<script setup>
|
|
665
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
666
|
+
import { useAuthGuardRuntime } from "@jskit-ai/auth-web/client";
|
|
667
|
+
|
|
668
|
+
const authGuardRuntime = useAuthGuardRuntime({
|
|
669
|
+
required: true
|
|
670
|
+
});
|
|
671
|
+
const authState = ref(authGuardRuntime.getState());
|
|
672
|
+
let unsubscribe = null;
|
|
673
|
+
|
|
674
|
+
const isAuthenticated = computed(() => authState.value?.authenticated === true);
|
|
675
|
+
|
|
676
|
+
onMounted(() => {
|
|
677
|
+
unsubscribe = authGuardRuntime.subscribe((nextState) => {
|
|
678
|
+
authState.value = nextState;
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
onBeforeUnmount(() => {
|
|
683
|
+
if (typeof unsubscribe === "function") {
|
|
684
|
+
unsubscribe();
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
</script>
|
|
688
|
+
|
|
689
|
+
<template>
|
|
690
|
+
<section class="pa-4">
|
|
691
|
+
<v-alert v-if="isAuthenticated" type="success" variant="tonal" class="mb-4">
|
|
692
|
+
You are logged in!
|
|
693
|
+
</v-alert>
|
|
694
|
+
|
|
695
|
+
<h1 class="text-h5 mb-2">Home</h1>
|
|
696
|
+
<p class="text-body-2 text-medium-emphasis">Replace this scaffold with your page implementation.</p>
|
|
697
|
+
</section>
|
|
698
|
+
</template>
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
That code works, and it shows exactly what `useAuthStore()` is wrapping:
|
|
702
|
+
|
|
703
|
+
- `getState()` gives the first auth snapshot immediately
|
|
704
|
+
- `subscribe(...)` keeps that snapshot updated later
|
|
705
|
+
- the component turns that imperative runtime into normal Vue refs and computeds
|
|
706
|
+
|
|
707
|
+
For ordinary Vue component code there is usually no advantage to writing it this way. `useAuthStore()` already gives you the same surfaced information plus the same runtime methods when you need them. The direct runtime version is mainly worth knowing so you understand the lower-level contract that `auth-web` itself is building on.
|
|
708
|
+
|
|
709
|
+
### Who actually talks to whom
|
|
710
|
+
|
|
711
|
+
The most important thing to understand is that the browser usually talks to **your app**, and your app talks to **Supabase**.
|
|
712
|
+
|
|
713
|
+
For the normal email-and-password flow, the browser does **not** call Supabase directly. It posts to the app's own API routes such as `/api/login`. The JSKIT server then calls Supabase, receives the Supabase session, and turns that into HTTP-only cookies.
|
|
714
|
+
|
|
715
|
+
That means there are really three actors in play:
|
|
716
|
+
|
|
717
|
+
- the browser, which renders the login screen and submits forms
|
|
718
|
+
- the JSKIT app server, which owns `/api/login`, `/api/oauth/complete`, `/api/session`, and the cookie-writing step
|
|
719
|
+
- Supabase, which owns the real authentication backend, password verification, OAuth exchange, and auth user records
|
|
720
|
+
|
|
721
|
+
So the mental model should be:
|
|
722
|
+
|
|
723
|
+
```text
|
|
724
|
+
browser -> JSKIT app -> Supabase
|
|
725
|
+
browser <- JSKIT app <- Supabase
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
For OAuth there is one extra bounce: the browser is redirected out to Supabase and then back again. But even there, the app still owns the start and completion steps.
|
|
729
|
+
|
|
730
|
+
### Password login: the full round trip
|
|
731
|
+
|
|
732
|
+
The simplest login flow is the normal `Email + Password` form.
|
|
733
|
+
|
|
734
|
+
On the client side, `DefaultLoginView` eventually calls `useLoginViewActions.submitAuth()`. In sign-in mode that becomes a `POST` to:
|
|
735
|
+
|
|
736
|
+
```text
|
|
737
|
+
/api/login
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
with a body shaped roughly like this:
|
|
741
|
+
|
|
742
|
+
```json
|
|
743
|
+
{
|
|
744
|
+
"email": "alice@example.com",
|
|
745
|
+
"password": "correct horse battery staple"
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
From there, the server-side flow is:
|
|
750
|
+
|
|
751
|
+
1. `POST /api/login` hits the route registered by `auth-web`.
|
|
752
|
+
2. `AuthController.login()` receives the request.
|
|
753
|
+
3. `AuthWebService.login()` executes the internal action `auth.login.password`.
|
|
754
|
+
4. The Supabase auth service calls `supabase.auth.signInWithPassword(...)`.
|
|
755
|
+
5. Supabase returns a `user` object and a `session` object.
|
|
756
|
+
6. JSKIT syncs the app-side profile mirror from that Supabase user.
|
|
757
|
+
7. JSKIT writes the access and refresh tokens into HTTP-only cookies.
|
|
758
|
+
8. The API response sent back to the browser is intentionally small.
|
|
759
|
+
|
|
760
|
+
The important detail is step 7. The browser does **not** receive the raw Supabase session tokens as normal application state. The server writes them into cookies instead:
|
|
761
|
+
|
|
762
|
+
- `sb_access_token`
|
|
763
|
+
- `sb_refresh_token`
|
|
764
|
+
|
|
765
|
+
Those cookies are HTTP-only and `sameSite: "lax"`, so the browser sends them back automatically on later requests, but normal client-side code cannot read them directly.
|
|
766
|
+
|
|
767
|
+
The JSON response from `/api/login` is much smaller than the underlying Supabase session object:
|
|
768
|
+
|
|
769
|
+
```json
|
|
770
|
+
{
|
|
771
|
+
"ok": true,
|
|
772
|
+
"username": "alice"
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
After that, the browser still is not done. The login view immediately calls `/api/session` to confirm the session and fetch the current auth state. If that session check comes back with `authenticated: true`, the client redirects to the requested `returnTo` path.
|
|
777
|
+
|
|
778
|
+
So the real password-login round trip is:
|
|
779
|
+
|
|
780
|
+
```text
|
|
781
|
+
1. browser -> POST /api/login -> JSKIT app
|
|
782
|
+
2. JSKIT app -> supabase.auth.signInWithPassword(...)
|
|
783
|
+
3. Supabase -> JSKIT app: user + session
|
|
784
|
+
4. JSKIT app -> browser: set HTTP-only cookies + { ok, username }
|
|
785
|
+
5. browser -> GET /api/session
|
|
786
|
+
6. JSKIT app -> browser: { authenticated, username, csrfToken, ... }
|
|
787
|
+
7. browser redirects to the requested route
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### OAuth login: the extra browser bounce
|
|
791
|
+
|
|
792
|
+
OAuth is the case where the browser really does leave the app briefly, but the app still owns the edges of the flow.
|
|
793
|
+
|
|
794
|
+
The first step is still browser -> app. If the login page shows a button such as `Continue with Google`, clicking it does **not** go straight to Supabase. It first goes to:
|
|
795
|
+
|
|
796
|
+
```text
|
|
797
|
+
/api/oauth/google/start?returnTo=/home
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
That server route does three jobs:
|
|
801
|
+
|
|
802
|
+
- normalizes the provider id
|
|
803
|
+
- normalizes the `returnTo` path
|
|
804
|
+
- asks Supabase for the correct provider redirect URL
|
|
805
|
+
|
|
806
|
+
JSKIT then redirects the browser to Supabase's OAuth flow. The redirect URL that JSKIT asks Supabase to use points back to your app, usually `/auth/login`, with some query parameters describing the provider and the intended return target.
|
|
807
|
+
|
|
808
|
+
So the browser flow becomes:
|
|
809
|
+
|
|
810
|
+
```text
|
|
811
|
+
browser -> /api/oauth/google/start
|
|
812
|
+
app -> Supabase OAuth redirect URL
|
|
813
|
+
browser -> Supabase / provider login page
|
|
814
|
+
Supabase -> browser back to /auth/login?...callback params...
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
When the browser lands back on `/auth/login`, the login page JavaScript inspects the URL. It looks for either:
|
|
818
|
+
|
|
819
|
+
- an OAuth `code`, or
|
|
820
|
+
- an access/refresh token pair
|
|
821
|
+
|
|
822
|
+
If it finds them, it does **not** treat the browser as fully signed in yet. Instead, it posts a small completion payload back to the app at:
|
|
823
|
+
|
|
824
|
+
```text
|
|
825
|
+
/api/oauth/complete
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
That payload looks roughly like one of these:
|
|
829
|
+
|
|
830
|
+
```json
|
|
831
|
+
{
|
|
832
|
+
"provider": "google",
|
|
833
|
+
"code": "..."
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
or
|
|
838
|
+
|
|
839
|
+
```json
|
|
840
|
+
{
|
|
841
|
+
"provider": "google",
|
|
842
|
+
"accessToken": "...",
|
|
843
|
+
"refreshToken": "..."
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Now the app server finishes the job:
|
|
848
|
+
|
|
849
|
+
1. `AuthController.oauthComplete()` receives the payload.
|
|
850
|
+
2. The Supabase auth service either:
|
|
851
|
+
- exchanges the code with `supabase.auth.exchangeCodeForSession(...)`, or
|
|
852
|
+
- restores the session with `supabase.auth.setSession(...)`
|
|
853
|
+
3. Supabase returns `user` and `session`.
|
|
854
|
+
4. JSKIT syncs the local profile mirror.
|
|
855
|
+
5. JSKIT writes HTTP-only cookies.
|
|
856
|
+
6. The browser strips the callback params out of the URL.
|
|
857
|
+
7. The browser refreshes `/api/session`.
|
|
858
|
+
8. The browser redirects to `returnTo`.
|
|
859
|
+
|
|
860
|
+
So the full OAuth dance is:
|
|
861
|
+
|
|
862
|
+
```text
|
|
863
|
+
browser -> app start route
|
|
864
|
+
app -> Supabase redirect
|
|
865
|
+
browser -> Supabase/provider
|
|
866
|
+
Supabase/provider -> browser back to /auth/login
|
|
867
|
+
browser -> app completion route
|
|
868
|
+
app -> Supabase session exchange
|
|
869
|
+
Supabase -> app: user + session
|
|
870
|
+
app -> browser: cookies + small success payload
|
|
871
|
+
browser -> /api/session -> redirect
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
That is why the login page needs both browser-side logic and server-side routes. The browser owns the redirect dance, but the app still owns the final session establishment step.
|
|
875
|
+
|
|
876
|
+
### What `/api/session` is really doing
|
|
877
|
+
|
|
878
|
+
`/api/session` is more than a yes-or-no login check. It is the app's current auth truth endpoint.
|
|
879
|
+
|
|
880
|
+
When the browser calls it, the server:
|
|
881
|
+
|
|
882
|
+
- reads the auth cookies
|
|
883
|
+
- checks whether the access token still looks valid
|
|
884
|
+
- refreshes the session through Supabase if needed
|
|
885
|
+
- clears invalid cookies if the session is no longer usable
|
|
886
|
+
- returns the auth state the client actually needs
|
|
887
|
+
|
|
888
|
+
The response is shaped roughly like this:
|
|
889
|
+
|
|
890
|
+
```json
|
|
891
|
+
{
|
|
892
|
+
"authenticated": true,
|
|
893
|
+
"username": "alice",
|
|
894
|
+
"csrfToken": "...",
|
|
895
|
+
"oauthProviders": [],
|
|
896
|
+
"oauthDefaultProvider": null
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
That explains why the login screen and auth guard runtime both care about `/api/session`. It is how the browser learns:
|
|
901
|
+
|
|
902
|
+
- whether the user is authenticated
|
|
903
|
+
- which username to show
|
|
904
|
+
- which OAuth buttons to render
|
|
905
|
+
- which CSRF token to use for later writes
|
|
906
|
+
|
|
907
|
+
It is also why the shell widget can react cleanly to auth state without storing raw session tokens in client state. The browser just asks the app for the current session view, and the app derives that from its cookies plus Supabase.
|
|
908
|
+
|
|
909
|
+
## What appears in Supabase
|
|
910
|
+
|
|
911
|
+
It is important to separate **Supabase auth data** from **JSKIT app-owned data**.
|
|
912
|
+
|
|
913
|
+
When a user registers from this screen, Supabase creates a real auth user immediately. According to Supabase's user-management docs, you can see users in two places.
|
|
914
|
+
|
|
915
|
+
- `Authentication -> Users` in the Supabase dashboard
|
|
916
|
+
- the `auth` schema in the Table Editor
|
|
917
|
+
|
|
918
|
+
In practice, after someone registers you should expect to see at least these things on the Supabase side.
|
|
919
|
+
|
|
920
|
+
- A user row exists in `auth.users`.
|
|
921
|
+
- The email address appears there.
|
|
922
|
+
- Supabase tracks whether the email has been confirmed yet.
|
|
923
|
+
- The user has a stable auth id.
|
|
924
|
+
- The provider is Supabase email/password unless you later add OAuth.
|
|
925
|
+
|
|
926
|
+
On the Dashboard's `Authentication -> Users` page, that usually means you will see a new user entry with the email address, creation time, last sign-in information once they have signed in, and confirmation state. If you open the user details, you can inspect the auth record more closely.
|
|
927
|
+
|
|
928
|
+
JSKIT's register flow also sends a starter `display_name` into Supabase user metadata. That means the new user can carry an initial display-name value in provider metadata even before the later database-backed users layer is installed.
|
|
929
|
+
|
|
930
|
+
This is the key distinction for the chapter.
|
|
931
|
+
|
|
932
|
+
- Supabase already has a real user record.
|
|
933
|
+
- JSKIT's own mirrored profile/settings store is still the temporary standalone in-memory version.
|
|
934
|
+
|
|
935
|
+
So if you restart the local JSKIT server in this chapter, the temporary app-side mirror is rebuilt as users authenticate again, but the actual Supabase auth user is still there because that data lives in Supabase, not in your local Node process.
|
|
936
|
+
|
|
937
|
+
One more subtle point matters here.
|
|
938
|
+
|
|
939
|
+
- If registration requires email confirmation, the user can already appear in Supabase even though the browser is not signed in yet.
|
|
940
|
+
- If confirmation is disabled, the user appears in Supabase and gets an active session immediately.
|
|
941
|
+
|
|
942
|
+
That is why the confirmation screen in the app should be understood as a **session-state** difference, not a sign that the user was never created.
|
|
943
|
+
|
|
944
|
+
## Summary
|
|
945
|
+
|
|
946
|
+
After this chapter, the app can really authenticate against Supabase. It has a public `auth` surface, a stock login page, a sign-out route, and a shell widget that reflects auth state. The provider-specific values now live in `.env`, and the web auth layer is wired into the same placement and surface system introduced earlier in the guide.
|
|
947
|
+
|
|
948
|
+
Just as importantly, the app is still deliberately incomplete. Authentication exists, but the database-backed user model does not. That separation is useful, because the next layer of the guide can explain users and persistence without having to also explain the first auth setup at the same time.
|