miolo 3.0.0-beta.154 → 3.0.0-beta.155
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/bin/create/index.mjs +1 -1
- package/bin/create/validation.mjs +1 -1
- package/package.json +4 -3
- package/src/config/.env +19 -8
- package/src/config/defaults.mjs +28 -21
- package/src/config/index.mjs +10 -17
- package/src/engines/cron/syscheck.mjs +2 -2
- package/src/middleware/auth/basic.mjs +6 -4
- package/src/middleware/auth/guest.mjs +1 -0
- package/src/middleware/auth/passport/index.mjs +299 -0
- package/src/middleware/ssr/ssr_render.mjs +4 -2
- package/src/server.mjs +5 -10
- package/template/.agent/skills/miolo-auth/SKILL.md +273 -118
- package/template/.agent/skills/miolo-session-context/SKILL.md +4 -3
- package/template/.env +9 -2
- package/template/db/sql/01_users.sql +3 -0
- package/template/package.json +7 -7
- package/template/src/cli/pages/offline/Login.jsx +0 -3
- package/template/src/cli/pages/offline/LoginForm.jsx +89 -79
- package/template/src/cli/pages/todos/context/TodosProvider.jsx +2 -2
- package/template/src/server/db/io/users/auth.mjs +36 -4
- package/template/src/server/db/triggers/user.mjs +5 -4
- package/template/src/server/miolo/auth/passport.mjs +93 -0
- package/template/src/server/miolo/db.mjs +4 -15
- package/template/src/server/miolo/index.mjs +4 -2
- package/src/middleware/auth/credentials/index.mjs +0 -183
- package/template/.env.production +0 -111
- package/template/src/server/miolo/auth/credentials.mjs +0 -47
- /package/src/middleware/auth/{credentials → passport}/session/index.mjs +0 -0
- /package/src/middleware/auth/{credentials → passport}/session/store.mjs +0 -0
- /package/src/middleware/auth/{credentials → passport}/session/store_koa_redis.mjs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: miolo-auth
|
|
3
|
-
description: Authentication configuration and strategies for miolo applications. Use when implementing, modifying, or troubleshooting authentication, configuring auth strategies (
|
|
3
|
+
description: Authentication configuration and strategies for miolo applications. Use when implementing, modifying, or troubleshooting authentication, configuring auth strategies (passport, basic, guest), or setting up user sessions.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Miolo Authentication
|
|
@@ -9,14 +9,13 @@ Authentication configuration and strategies for miolo applications.
|
|
|
9
9
|
|
|
10
10
|
## Authentication Strategies
|
|
11
11
|
|
|
12
|
-
Miolo supports multiple built-in authentication strategies
|
|
12
|
+
Miolo supports multiple built-in authentication strategies:
|
|
13
13
|
|
|
14
14
|
```
|
|
15
15
|
src/server/miolo/auth/
|
|
16
|
-
├──
|
|
16
|
+
├── passport.mjs # Passport-based auth (local + OAuth2)
|
|
17
17
|
├── basic.mjs # HTTP Basic authentication
|
|
18
|
-
|
|
19
|
-
└── custom.mjs # Custom authentication logic
|
|
18
|
+
└── guest.mjs # Guest/anonymous access
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
## Configuring Authentication
|
|
@@ -24,56 +23,189 @@ src/server/miolo/auth/
|
|
|
24
23
|
Authentication is configured in `src/server/miolo/index.mjs`:
|
|
25
24
|
|
|
26
25
|
```javascript
|
|
27
|
-
import
|
|
26
|
+
import passport from './auth/passport.mjs'
|
|
28
27
|
// or: import auth from './auth/basic.mjs'
|
|
29
28
|
// or: import auth from './auth/guest.mjs'
|
|
30
29
|
|
|
31
30
|
export default {
|
|
32
|
-
auth
|
|
31
|
+
auth: {
|
|
32
|
+
passport // Enables Passport.js with local + Google OAuth2
|
|
33
|
+
},
|
|
33
34
|
// ... other config
|
|
34
35
|
}
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
##
|
|
38
|
+
## Passport Strategy (Recommended)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
Unified authentication using Passport.js supporting both **local (username/password)** and **Google OAuth2**.
|
|
40
41
|
|
|
41
|
-
**File:** `src/server/miolo/auth/
|
|
42
|
+
**File:** `src/server/miolo/auth/passport.mjs`
|
|
42
43
|
|
|
43
44
|
```javascript
|
|
44
|
-
import {
|
|
45
|
+
import { db_find_user_by_id, db_auth_user,
|
|
46
|
+
db_user_find_or_create_from_google } from '#server/db/io/users/auth.mjs'
|
|
47
|
+
|
|
48
|
+
const get_user_id = (user, done, ctx) => {
|
|
49
|
+
const uid = user?.id
|
|
50
|
+
if (uid != undefined) {
|
|
51
|
+
return done(null, uid)
|
|
52
|
+
} else {
|
|
53
|
+
const err = new Error('User id is undefined')
|
|
54
|
+
return done(err, null)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
const find_user_by_id = (id, done, ctx) => {
|
|
59
|
+
ctx.miolo.logger.debug(`[auth] find_user_by_id() - Searching user id ${id}`)
|
|
60
|
+
db_find_user_by_id(ctx.miolo, id).then(user => {
|
|
61
|
+
if (user == undefined) {
|
|
62
|
+
const err = new Error('User not found')
|
|
63
|
+
return done(err, null)
|
|
64
|
+
} else {
|
|
65
|
+
return done(null, user)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
70
|
+
const local_auth_user = (username, password, done, ctx) => {
|
|
71
|
+
ctx.miolo.logger.debug(`[auth][local] Checking credentials for ${username}`)
|
|
72
|
+
|
|
73
|
+
db_auth_user(ctx.miolo, username, password).then(([user, msg]) => {
|
|
74
|
+
if (user == undefined) {
|
|
75
|
+
const err = new Error(msg)
|
|
76
|
+
return done(err, null)
|
|
77
|
+
} else {
|
|
78
|
+
return done(null, user)
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
const google_auth_user = async (accessToken, refreshToken, profile, done, ctx) => {
|
|
84
|
+
try {
|
|
85
|
+
const google_id = profile.id
|
|
86
|
+
const email = profile.emails?.[0]?.value
|
|
87
|
+
const name = profile.displayName
|
|
88
|
+
const google_picture = profile.photos?.[0]?.value
|
|
89
|
+
|
|
90
|
+
ctx.miolo.logger.info(`[auth][google] Authenticated user: ${email}`)
|
|
91
|
+
|
|
92
|
+
return db_user_find_or_create_from_google(
|
|
93
|
+
ctx.miolo, email, name, google_id, google_picture
|
|
94
|
+
).then(([user, msg]) => {
|
|
95
|
+
if (user == undefined) {
|
|
96
|
+
const err = new Error(msg)
|
|
97
|
+
return done(err, null)
|
|
98
|
+
} else {
|
|
99
|
+
return done(null, user)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
} catch (error) {
|
|
103
|
+
ctx.miolo.logger.error(`[auth][google] Error: ${error}`)
|
|
104
|
+
return done(error, null)
|
|
68
105
|
}
|
|
69
106
|
}
|
|
107
|
+
|
|
108
|
+
export default {
|
|
109
|
+
get_user_id,
|
|
110
|
+
find_user_by_id,
|
|
111
|
+
local_auth_user,
|
|
112
|
+
local_url_login: '/login',
|
|
113
|
+
local_url_logout: '/logout',
|
|
114
|
+
local_url_login_redirect: undefined,
|
|
115
|
+
local_url_logout_redirect: '/',
|
|
116
|
+
google_auth_user,
|
|
117
|
+
google_client_id: process.env.MIOLO_AUTH_GOOGLE_CLIENT_ID,
|
|
118
|
+
google_client_secret: process.env.MIOLO_AUTH_GOOGLE_CLIENT_SECRET,
|
|
119
|
+
google_url_login: '/auth/google',
|
|
120
|
+
google_url_callback: process.env.MIOLO_AUTH_GOOGLE_CALLBACK_URL || '/auth/google/callback',
|
|
121
|
+
google_url_logout: '/logout',
|
|
122
|
+
google_url_logout_redirect: '/'
|
|
123
|
+
}
|
|
70
124
|
```
|
|
71
125
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `
|
|
76
|
-
-
|
|
126
|
+
### Passport Configuration Elements
|
|
127
|
+
|
|
128
|
+
#### Common Functions
|
|
129
|
+
- `get_user_id(user, done, ctx)` - Extracts user ID for session storage
|
|
130
|
+
- `find_user_by_id(id, done, ctx)` - Retrieves user by ID (session deserialization)
|
|
131
|
+
|
|
132
|
+
#### Local Authentication (Username/Password)
|
|
133
|
+
- `local_auth_user(username, password, done, ctx)` - Validates credentials
|
|
134
|
+
- `local_url_login` - Login endpoint (default: `/login`)
|
|
135
|
+
- `local_url_logout` - Logout endpoint (default: `/logout`)
|
|
136
|
+
- `local_url_login_redirect` - Redirect after login (optional)
|
|
137
|
+
- `local_url_logout_redirect` - Redirect after logout (default: `/`)
|
|
138
|
+
|
|
139
|
+
#### Google OAuth2 Authentication
|
|
140
|
+
- `google_auth_user(accessToken, refreshToken, profile, done, ctx)` - Handles Google profile
|
|
141
|
+
- `google_client_id` - Google OAuth2 client ID (from env)
|
|
142
|
+
- `google_client_secret` - Google OAuth2 client secret (from env)
|
|
143
|
+
- `google_url_login` - OAuth initiation endpoint (default: `/auth/google`)
|
|
144
|
+
- `google_url_callback` - OAuth callback endpoint (from env or `/auth/google/callback`)
|
|
145
|
+
- `google_url_logout` - Logout endpoint (default: `/logout`)
|
|
146
|
+
- `google_url_logout_redirect` - Redirect after logout (default: `/`)
|
|
147
|
+
|
|
148
|
+
### Environment Variables
|
|
149
|
+
|
|
150
|
+
**Required for Google OAuth2 (.env):**
|
|
151
|
+
```
|
|
152
|
+
MIOLO_AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
153
|
+
MIOLO_AUTH_GOOGLE_CLIENT_SECRET=your-client-secret
|
|
154
|
+
MIOLO_AUTH_GOOGLE_CALLBACK_URL=http://localhost:8001/auth/google/callback
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Session configuration:**
|
|
158
|
+
```
|
|
159
|
+
MIOLO_SESSION_SALT=your-random-salt-here
|
|
160
|
+
MIOLO_SESSION_SECRET=your-session-secret
|
|
161
|
+
MIOLO_SESSION_SAME_SITE=lax # Required for Google OAuth2
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Google Cloud Console Setup
|
|
165
|
+
|
|
166
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
167
|
+
2. Create a project or select existing one
|
|
168
|
+
3. Enable Google+ API
|
|
169
|
+
4. Create OAuth 2.0 credentials:
|
|
170
|
+
- Application type: Web application
|
|
171
|
+
- Authorized JavaScript origins: `http://localhost:8001`
|
|
172
|
+
- Authorized redirect URIs: `http://localhost:8001/auth/google/callback`
|
|
173
|
+
5. Copy Client ID and Client Secret to your `.env`
|
|
174
|
+
|
|
175
|
+
### OAuth2 Flow
|
|
176
|
+
|
|
177
|
+
1. User clicks "Login with Google" → `window.location.href = '/auth/google'`
|
|
178
|
+
2. Server redirects to Google authentication page
|
|
179
|
+
3. User authenticates with Google
|
|
180
|
+
4. Google redirects back to `/auth/google/callback`
|
|
181
|
+
5. `google_auth_user` processes Google profile
|
|
182
|
+
6. User found/created in database
|
|
183
|
+
7. User logged in and redirected to home
|
|
184
|
+
|
|
185
|
+
### Frontend Login Button
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
// For local login (POST form)
|
|
189
|
+
<form onSubmit={handleLocalLogin}>
|
|
190
|
+
<input name="username" />
|
|
191
|
+
<input name="password" type="password" />
|
|
192
|
+
<button type="submit">Login</button>
|
|
193
|
+
</form>
|
|
194
|
+
|
|
195
|
+
// For Google OAuth2 (Navigate to endpoint)
|
|
196
|
+
<button onClick={() => window.location.href = '/auth/google'}>
|
|
197
|
+
Login with Google
|
|
198
|
+
</button>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**IMPORTANT:** Google OAuth2 requires **navigation**, not fetch/AJAX:
|
|
202
|
+
```javascript
|
|
203
|
+
// ✅ CORRECT
|
|
204
|
+
window.location.href = '/auth/google'
|
|
205
|
+
|
|
206
|
+
// ❌ WRONG - Causes CORS errors
|
|
207
|
+
fetch('/auth/google')
|
|
208
|
+
```
|
|
77
209
|
|
|
78
210
|
## Basic Authentication
|
|
79
211
|
|
|
@@ -86,7 +218,6 @@ export default {
|
|
|
86
218
|
async login(ctx, credentials) {
|
|
87
219
|
const { username, password } = credentials
|
|
88
220
|
|
|
89
|
-
// Validate against environment or database
|
|
90
221
|
if (username === process.env.API_USER &&
|
|
91
222
|
password === process.env.API_PASSWORD) {
|
|
92
223
|
return {
|
|
@@ -100,19 +231,16 @@ export default {
|
|
|
100
231
|
}
|
|
101
232
|
```
|
|
102
233
|
|
|
103
|
-
## Guest
|
|
234
|
+
## Guest Authentication
|
|
104
235
|
|
|
105
|
-
|
|
236
|
+
Anonymous access without credentials.
|
|
106
237
|
|
|
107
238
|
**File:** `src/server/miolo/auth/guest.mjs`
|
|
108
239
|
|
|
109
240
|
```javascript
|
|
110
241
|
export default {
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
ok: true,
|
|
114
|
-
user: { id: 0, username: 'guest', role: 'guest' }
|
|
115
|
-
}
|
|
242
|
+
make_guest_token: (session) => {
|
|
243
|
+
return `guest-${Date.now()}-${Math.random()}`
|
|
116
244
|
}
|
|
117
245
|
}
|
|
118
246
|
```
|
|
@@ -124,12 +252,15 @@ After successful login, user is available in routes:
|
|
|
124
252
|
```javascript
|
|
125
253
|
export async function r_protected_route(ctx, params) {
|
|
126
254
|
const user = ctx.state.user
|
|
255
|
+
const isAuthenticated = ctx.session.authenticated
|
|
127
256
|
|
|
128
257
|
// User object contains:
|
|
129
258
|
// - id
|
|
130
|
-
// - username
|
|
259
|
+
// - username (for local auth)
|
|
131
260
|
// - email
|
|
132
|
-
// -
|
|
261
|
+
// - name
|
|
262
|
+
// - google_id (for Google auth)
|
|
263
|
+
// - google_picture (for Google auth)
|
|
133
264
|
|
|
134
265
|
ctx.miolo.logger.info(`User ${user.id} accessing route`)
|
|
135
266
|
|
|
@@ -160,36 +291,91 @@ export default [{
|
|
|
160
291
|
}]
|
|
161
292
|
```
|
|
162
293
|
|
|
163
|
-
## Database
|
|
294
|
+
## Database Functions
|
|
164
295
|
|
|
165
|
-
User
|
|
296
|
+
### User Authentication (Local)
|
|
166
297
|
|
|
167
298
|
**File:** `src/server/db/io/users/auth.mjs`
|
|
168
299
|
|
|
169
300
|
```javascript
|
|
170
301
|
import { sha512 } from '#server/utils/crypt.mjs'
|
|
171
302
|
|
|
172
|
-
export async function
|
|
173
|
-
const
|
|
303
|
+
export async function db_auth_user(miolo, username, password) {
|
|
304
|
+
const conn = miolo.db
|
|
305
|
+
const options = { transaction: undefined }
|
|
174
306
|
|
|
175
307
|
const salt = process.env.MIOLO_SESSION_SALT
|
|
176
308
|
const hashedPassword = sha512(password, salt)
|
|
177
309
|
|
|
178
|
-
const
|
|
179
|
-
SELECT id, username, email,
|
|
310
|
+
const query = `
|
|
311
|
+
SELECT id, username, name, email, active, admin
|
|
312
|
+
FROM u_user
|
|
313
|
+
WHERE username = $1 AND password = $2 AND active = 1`
|
|
314
|
+
|
|
315
|
+
const ruser = await conn.selectOne(query, [username, hashedPassword], options)
|
|
316
|
+
|
|
317
|
+
if (ruser?.id == undefined) {
|
|
318
|
+
return [undefined, 'Invalid credentials']
|
|
319
|
+
} else {
|
|
320
|
+
return [ruser, undefined]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### User by ID
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
export async function db_find_user_by_id(miolo, id) {
|
|
329
|
+
const conn = miolo.db
|
|
330
|
+
const options = { transaction: undefined }
|
|
331
|
+
|
|
332
|
+
const query = `
|
|
333
|
+
SELECT id, username, name, email, active, admin,
|
|
334
|
+
google_id, google_picture
|
|
180
335
|
FROM u_user
|
|
181
|
-
WHERE
|
|
182
|
-
`, [username, hashedPassword])
|
|
336
|
+
WHERE id = $1`
|
|
183
337
|
|
|
184
|
-
|
|
338
|
+
const ruser = await conn.selectOne(query, [id], options)
|
|
339
|
+
return ruser
|
|
185
340
|
}
|
|
186
341
|
```
|
|
187
342
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
343
|
+
### Google OAuth2 User
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
export async function db_user_find_or_create_from_google(
|
|
347
|
+
miolo, email, name, google_id, google_picture
|
|
348
|
+
) {
|
|
349
|
+
const conn = miolo.db
|
|
350
|
+
const options = { transaction: undefined }
|
|
351
|
+
|
|
352
|
+
// Try to find existing user by google_id
|
|
353
|
+
const query = `
|
|
354
|
+
SELECT id, username, name, email, active, admin,
|
|
355
|
+
google_id, google_picture
|
|
356
|
+
FROM u_user
|
|
357
|
+
WHERE google_id = $1`
|
|
358
|
+
|
|
359
|
+
const ruser = await conn.selectOne(query, [google_id], options)
|
|
360
|
+
|
|
361
|
+
if (ruser?.id == undefined) {
|
|
362
|
+
// Create new user
|
|
363
|
+
const UUser = await conn.get_model('u_user')
|
|
364
|
+
const data = {
|
|
365
|
+
name,
|
|
366
|
+
email,
|
|
367
|
+
google_id,
|
|
368
|
+
google_picture,
|
|
369
|
+
active: 1
|
|
370
|
+
}
|
|
371
|
+
const uid = await UUser.insert(data, options)
|
|
372
|
+
data.id = uid
|
|
373
|
+
return [data, undefined]
|
|
374
|
+
} else {
|
|
375
|
+
return [ruser, undefined]
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
193
379
|
|
|
194
380
|
## Password Hashing
|
|
195
381
|
|
|
@@ -207,71 +393,38 @@ const hashedPassword = sha512(plainPassword, salt)
|
|
|
207
393
|
MIOLO_SESSION_SALT=your-random-salt-here
|
|
208
394
|
```
|
|
209
395
|
|
|
210
|
-
##
|
|
396
|
+
## Database Trigger for Password Hashing
|
|
211
397
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
**File:** `src/server/db/io/users/pwd.mjs`
|
|
398
|
+
**File:** `src/server/db/triggers/user.mjs`
|
|
215
399
|
|
|
216
400
|
```javascript
|
|
217
401
|
import { sha512 } from '#server/utils/crypt.mjs'
|
|
218
402
|
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// Verify old password
|
|
225
|
-
const user = await ctx.miolo.db.query(`
|
|
226
|
-
SELECT id FROM u_user
|
|
227
|
-
WHERE id = $1 AND password = $2
|
|
228
|
-
`, [user_id, sha512(old_password, salt)])
|
|
229
|
-
|
|
230
|
-
if (user.rows.length === 0) {
|
|
231
|
-
throw new Error('Invalid current password')
|
|
403
|
+
async function beforeInsertUser(conn, params, options) {
|
|
404
|
+
const raw_pwd = params?.password
|
|
405
|
+
if (raw_pwd) {
|
|
406
|
+
const cry_pwd = sha512(raw_pwd, process.env.MIOLO_SESSION_SALT)
|
|
407
|
+
params.password = cry_pwd
|
|
232
408
|
}
|
|
233
|
-
|
|
234
|
-
// Update to new password
|
|
235
|
-
const newHash = sha512(new_password, salt)
|
|
236
|
-
await ctx.miolo.db.query(`
|
|
237
|
-
UPDATE u_user
|
|
238
|
-
SET password = $1
|
|
239
|
-
WHERE id = $2
|
|
240
|
-
`, [newHash, user_id])
|
|
241
|
-
|
|
242
|
-
return { changed: true }
|
|
409
|
+
return [params, options, true]
|
|
243
410
|
}
|
|
244
|
-
```
|
|
245
411
|
|
|
246
|
-
|
|
412
|
+
export default {
|
|
413
|
+
before_insert: beforeInsertUser
|
|
414
|
+
}
|
|
415
|
+
```
|
|
247
416
|
|
|
248
|
-
|
|
417
|
+
## Session Configuration
|
|
249
418
|
|
|
250
|
-
**
|
|
419
|
+
**Important for OAuth2:** The session must use `sameSite: 'lax'` to work with Google redirects.
|
|
251
420
|
|
|
421
|
+
**In** `packages/miolo/src/config/defaults.mjs`:
|
|
252
422
|
```javascript
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
async login(ctx, credentials) {
|
|
261
|
-
// Custom logic: OAuth, LDAP, etc.
|
|
262
|
-
const user = await yourCustomAuthLogic(credentials)
|
|
263
|
-
|
|
264
|
-
if (!user) {
|
|
265
|
-
return { ok: false, error: 'Authentication failed' }
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return { ok: true, user }
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
async logout(ctx) {
|
|
272
|
-
// Custom cleanup
|
|
273
|
-
await yourCustomLogoutLogic(ctx)
|
|
274
|
-
return { ok: true }
|
|
423
|
+
session: {
|
|
424
|
+
options: {
|
|
425
|
+
sameSite: 'lax', // Required for OAuth2
|
|
426
|
+
secure: false, // Set true in production with HTTPS
|
|
427
|
+
maxAge: 86400000, // 24 hours
|
|
275
428
|
}
|
|
276
429
|
}
|
|
277
430
|
```
|
|
@@ -280,16 +433,18 @@ export default {
|
|
|
280
433
|
|
|
281
434
|
1. **Never store plain passwords** - Always hash with salt
|
|
282
435
|
2. **Use strong session salt** - Set `MIOLO_SESSION_SALT` in `.env`
|
|
283
|
-
3. **Don't return passwords** - User object should never include password
|
|
436
|
+
3. **Don't return passwords** - User object should never include password field
|
|
284
437
|
4. **Validate on server** - Never trust client-side authentication
|
|
285
438
|
5. **Use HTTPS in production** - Protect credentials in transit
|
|
286
|
-
6. **
|
|
287
|
-
7. **
|
|
439
|
+
6. **Set secure cookies in production** - `MIOLO_SESSION_SECURE=true`
|
|
440
|
+
7. **Use sameSite=lax for OAuth2** - Required for external OAuth redirects
|
|
441
|
+
8. **Rate limit login attempts** - Prevent brute force attacks
|
|
442
|
+
9. **Log authentication events** - Track successful and failed logins
|
|
288
443
|
|
|
289
444
|
## Examples from miolo-sample
|
|
290
445
|
|
|
291
446
|
See actual implementations:
|
|
292
|
-
- `src/server/miolo/auth/
|
|
293
|
-
- `src/server/db/io/users/auth.mjs` - User authentication
|
|
294
|
-
- `src/server/db/
|
|
447
|
+
- `src/server/miolo/auth/passport.mjs` - Passport authentication config
|
|
448
|
+
- `src/server/db/io/users/auth.mjs` - User authentication queries
|
|
449
|
+
- `src/server/db/triggers/user.mjs` - Password hashing trigger
|
|
295
450
|
- `src/server/utils/crypt.mjs` - Hashing utilities
|
|
@@ -59,11 +59,12 @@ export async function r_get_profile(ctx, params) {
|
|
|
59
59
|
**Available properties:**
|
|
60
60
|
- `ctx.session.user` - User object (or `null`)
|
|
61
61
|
- `ctx.session.authenticated` - Boolean authentication status
|
|
62
|
+
- `ctx.session.auth_method` - Authentication method (local, google, etc.)
|
|
62
63
|
- `ctx.session.token` - Auth token (guest auth only)
|
|
63
64
|
|
|
64
65
|
### Credentials Auth Methods
|
|
65
66
|
|
|
66
|
-
When using
|
|
67
|
+
When using local authentication strategy, additional methods are available:
|
|
67
68
|
|
|
68
69
|
```javascript
|
|
69
70
|
export async function r_user_login(ctx, params) {
|
|
@@ -99,14 +100,14 @@ export async function r_protected_route(ctx, params) {
|
|
|
99
100
|
}
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
**Available methods (
|
|
103
|
+
**Available methods (local auth):**
|
|
103
104
|
- `ctx.isAuthenticated()` - Returns `true` if user is authenticated
|
|
104
105
|
- `ctx.isUnauthenticated()` - Returns `true` if user is NOT authenticated
|
|
105
106
|
- `await ctx.login(username, password)` - Attempt to authenticate user
|
|
106
107
|
- `ctx.logout()` - End user session
|
|
107
108
|
|
|
108
109
|
**User access:**
|
|
109
|
-
- `ctx.state.user` - Authenticated user object (
|
|
110
|
+
- `ctx.state.user` - Authenticated user object (local auth)
|
|
110
111
|
- `ctx.session.user` - User object (all auth methods)
|
|
111
112
|
|
|
112
113
|
### Backend Usage Example
|
package/template/.env
CHANGED
|
@@ -21,6 +21,13 @@ MIOLO_REQUEST_SLOW=4 # seconds to consider slow a request
|
|
|
21
21
|
MIOLO_GEOIP_ENABLED=true
|
|
22
22
|
MIOLO_GEOIP_LOCAL_IPS=127.0.0.1,172.22.0.1,172.19.0.1
|
|
23
23
|
|
|
24
|
+
#
|
|
25
|
+
# If google auth
|
|
26
|
+
#
|
|
27
|
+
MIOLO_AUTH_GOOGLE_CLIENT_ID=123456
|
|
28
|
+
MIOLO_AUTH_GOOGLE_CLIENT_SECRET=123456
|
|
29
|
+
MIOLO_AUTH_GOOGLE_CALLBACK_URL=/auth/google/callback
|
|
30
|
+
|
|
24
31
|
#
|
|
25
32
|
# Session
|
|
26
33
|
#
|
|
@@ -30,7 +37,7 @@ MIOLO_SESSION_SECRET=00000000-0000-0000-0000-000000000000
|
|
|
30
37
|
MIOLO_SESSION_MAX_AGE=864000000
|
|
31
38
|
MIOLO_SESSION_SECURE=false
|
|
32
39
|
MIOLO_SESSION_RENEW=true
|
|
33
|
-
MIOLO_SESSION_SAME_SITE=
|
|
40
|
+
MIOLO_SESSION_SAME_SITE=lax # lax | strict
|
|
34
41
|
|
|
35
42
|
#
|
|
36
43
|
# Database
|
|
@@ -51,7 +58,7 @@ MIOLO_DB_POOL_IDLE_TIMEOUT_MS=10000
|
|
|
51
58
|
# Logging
|
|
52
59
|
#
|
|
53
60
|
|
|
54
|
-
MIOLO_LOG_LEVEL=
|
|
61
|
+
MIOLO_LOG_LEVEL=debug
|
|
55
62
|
MIOLO_LOG_CONSOLE_ENABLED=true
|
|
56
63
|
MIOLO_LOG_FILE_ENABLED=false
|
|
57
64
|
MIOLO_LOG_MAIL_ENABLED=false
|
package/template/package.json
CHANGED
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@radix-ui/react-separator": "^1.1.8",
|
|
37
37
|
"@radix-ui/react-slot": "^1.2.4",
|
|
38
38
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
39
|
-
"@stepperize/react": "^6.0.
|
|
40
|
-
"@tailwindcss/postcss": "^4.
|
|
39
|
+
"@stepperize/react": "^6.0.1",
|
|
40
|
+
"@tailwindcss/postcss": "^4.2.0",
|
|
41
41
|
"@tanstack/react-table": "^8.21.3",
|
|
42
42
|
"class-variance-authority": "^0.7.1",
|
|
43
43
|
"clsx": "^2.1.1",
|
|
44
44
|
"farrapa": "^3.0.0-beta.3",
|
|
45
45
|
"intre": "^3.0.0-beta.3",
|
|
46
46
|
"joi": "^18.0.2",
|
|
47
|
-
"lucide-react": "^0.
|
|
48
|
-
"miolo-cli": "^3.0.0-beta.
|
|
49
|
-
"miolo-react": "^3.0.0-beta.
|
|
47
|
+
"lucide-react": "^0.574.0",
|
|
48
|
+
"miolo-cli": "^3.0.0-beta.155",
|
|
49
|
+
"miolo-react": "^3.0.0-beta.155",
|
|
50
50
|
"next-themes": "^0.4.6",
|
|
51
51
|
"radix-ui": "^1.4.3",
|
|
52
52
|
"react": "^19.2.4",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"recharts": "^3.7.0",
|
|
56
56
|
"sonner": "^2.0.7",
|
|
57
57
|
"tailwind": "^4.0.0",
|
|
58
|
-
"tailwind-merge": "^3.4.
|
|
58
|
+
"tailwind-merge": "^3.4.1",
|
|
59
59
|
"tinguir": "^0.0.7",
|
|
60
60
|
"tw-animate-css": "^1.4.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"miolo": "^3.0.0-beta.
|
|
63
|
+
"miolo": "^3.0.0-beta.155",
|
|
64
64
|
"sass-embedded": "^1.97.3",
|
|
65
65
|
"xeira": "^2.0.0-beta.10"
|
|
66
66
|
},
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import useSessionContext from '#cli/context/session/useSessionContext.mjs'
|
|
4
3
|
import { LoginForm } from "./LoginForm.jsx"
|
|
5
4
|
|
|
6
5
|
const Login = () => {
|
|
7
|
-
const {login} = useSessionContext()
|
|
8
6
|
|
|
9
7
|
return (
|
|
10
8
|
<div className="grid min-h-svh lg:grid-cols-2">
|
|
@@ -29,7 +27,6 @@ const Login = () => {
|
|
|
29
27
|
<div className="flex flex-1 items-center justify-center">
|
|
30
28
|
<div className="w-full max-w-xs">
|
|
31
29
|
<LoginForm
|
|
32
|
-
onLogin = {login}
|
|
33
30
|
/* TODO orgotLink*/
|
|
34
31
|
/>
|
|
35
32
|
</div>
|