@adonis-agora/authkit-server 1.0.0-rc.7 → 1.0.0-rc.8
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/CHANGELOG.md +6 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -0
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/register_auth_host.d.ts +8 -0
- package/build/src/host/register_auth_host.js +112 -29
- package/build/src/host/ui-dist/assets/index-DlKYAhpy.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/build/src/host/ui-dist/assets/index-u0bMxkLg.js +0 -67
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
|
+
import { registerAgoraConsole } from '../console_switcher.js';
|
|
1
3
|
import { resolveRateLimit } from '../define_config.js';
|
|
2
4
|
import { accountHome } from './account_home.js';
|
|
3
5
|
import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
|
|
@@ -6,6 +8,7 @@ import { resolveAccountRoles } from './account_roles.js';
|
|
|
6
8
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
7
9
|
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
8
10
|
import { adminAuthorizeChecker, appInProduction } from './admin_authorize.js';
|
|
11
|
+
import { adminConsoleEntry } from './admin_console/admin_console_entry.js';
|
|
9
12
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
10
13
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
11
14
|
import { consoleDeniedPage } from './console_denied_page.js';
|
|
@@ -86,6 +89,23 @@ async function checkAndRefreshIdle(ctx) {
|
|
|
86
89
|
return false;
|
|
87
90
|
}
|
|
88
91
|
}
|
|
92
|
+
/** Read-only twin of {@link checkAndRefreshIdle}: whether the idle timeout passed. Never writes. */
|
|
93
|
+
async function isIdleExpired(ctx) {
|
|
94
|
+
try {
|
|
95
|
+
const runtimeSettings = await resolveRuntimeSettings(ctx);
|
|
96
|
+
if (!runtimeSettings)
|
|
97
|
+
return false;
|
|
98
|
+
const policy = await resolveEffectiveSessionPolicy(runtimeSettings);
|
|
99
|
+
const idleMs = policy.idleTimeoutMinutes * 60 * 1000;
|
|
100
|
+
if (idleMs <= 0)
|
|
101
|
+
return false;
|
|
102
|
+
const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
|
|
103
|
+
return lastSeen !== undefined && Date.now() - lastSeen > idleMs;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
89
109
|
/**
|
|
90
110
|
* Constrói a URL de redirect para o login da conta, preservando o destino
|
|
91
111
|
* original como `return_to` (URL-encoded). A URL de destino é o path + query
|
|
@@ -187,45 +207,65 @@ const accountGuard = async (ctx, next) => {
|
|
|
187
207
|
* As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
|
|
188
208
|
*/
|
|
189
209
|
export const adminGuard = async (ctx, next) => {
|
|
210
|
+
const access = await evaluateAdminAccess(ctx);
|
|
211
|
+
switch (access.kind) {
|
|
212
|
+
case 'allowed':
|
|
213
|
+
return next();
|
|
214
|
+
case 'disabled':
|
|
215
|
+
// Defesa contra flag-drift: as rotas são montadas com `admin: true` em tempo de
|
|
216
|
+
// registro, ANTES de o config resolver. Se o config tiver `admin.enabled: false`,
|
|
217
|
+
// as rotas existem mas o console deve estar desligado — 404 (não vaza a existência).
|
|
218
|
+
return ctx.response.notFound();
|
|
219
|
+
case 'agent':
|
|
220
|
+
return ctx.response.status(403).send('');
|
|
221
|
+
case 'signed-out':
|
|
222
|
+
// `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
|
|
223
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
224
|
+
case 'idle':
|
|
225
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
|
|
226
|
+
case 'not-admin':
|
|
227
|
+
// Evita vazar a existência do console admin: redireciona para o accountHome
|
|
228
|
+
// (sem mostrar a URL do admin).
|
|
229
|
+
if (wantsJson(ctx)) {
|
|
230
|
+
return ctx.response
|
|
231
|
+
.status(403)
|
|
232
|
+
.send({ error: { code: 'forbidden', message: 'Admin role required.' } });
|
|
233
|
+
}
|
|
234
|
+
return redirectExact(ctx.response, accountHome(access.cfg));
|
|
235
|
+
case 'denied':
|
|
236
|
+
return consoleDenied(ctx, access.decision);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* The admin console gate: the session, then `admin.roles`, then `admin.authorize`.
|
|
241
|
+
*
|
|
242
|
+
* `probe: true` is the console switcher asking from another console: it reads the idle timeout
|
|
243
|
+
* without refreshing or ending the session.
|
|
244
|
+
*/
|
|
245
|
+
async function evaluateAdminAccess(ctx, options = {}) {
|
|
190
246
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
191
247
|
const cfg = service.config;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// as rotas existem mas o console deve estar desligado — 404 (não vaza a existência).
|
|
195
|
-
if (!cfg.admin.enabled) {
|
|
196
|
-
return ctx.response.notFound();
|
|
197
|
-
}
|
|
248
|
+
if (!cfg.admin.enabled)
|
|
249
|
+
return { kind: 'disabled' };
|
|
198
250
|
if (isAgentBrowser(ctx))
|
|
199
|
-
return
|
|
251
|
+
return { kind: 'agent' };
|
|
200
252
|
// Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
|
|
201
253
|
await ensureConsoleSession(ctx);
|
|
202
254
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
203
|
-
if (!accountId)
|
|
204
|
-
|
|
205
|
-
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
206
|
-
}
|
|
255
|
+
if (!accountId)
|
|
256
|
+
return { kind: 'signed-out' };
|
|
207
257
|
// Idle timeout: também protege o console admin.
|
|
208
|
-
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
209
|
-
if (idleExpired)
|
|
210
|
-
return
|
|
211
|
-
}
|
|
258
|
+
const idleExpired = options.probe ? await isIdleExpired(ctx) : await checkAndRefreshIdle(ctx);
|
|
259
|
+
if (idleExpired)
|
|
260
|
+
return { kind: 'idle' };
|
|
212
261
|
const allowed = cfg.admin.roles;
|
|
213
262
|
const account = await cfg.accountStore.findById(accountId);
|
|
214
263
|
// Resolve roles through the host's role authority (`resolveTokenRoles`) when set — the same source
|
|
215
264
|
// the token claim is minted from — so an app-role admin reaches the console. Falls back to the
|
|
216
265
|
// account's stored `globalRoles` when no hook is configured.
|
|
217
266
|
const roles = account ? await resolveAccountRoles(cfg, account) : [];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// Evita vazar a existência do console admin: redireciona para o accountHome
|
|
221
|
-
// (sem mostrar a URL do admin).
|
|
222
|
-
if (wantsJson(ctx)) {
|
|
223
|
-
return ctx.response
|
|
224
|
-
.status(403)
|
|
225
|
-
.send({ error: { code: 'forbidden', message: 'Admin role required.' } });
|
|
226
|
-
}
|
|
227
|
-
return redirectExact(ctx.response, accountHome(cfg));
|
|
228
|
-
}
|
|
267
|
+
if (!roles.some((r) => allowed.includes(r)))
|
|
268
|
+
return { kind: 'not-admin', cfg };
|
|
229
269
|
// `admin.authorize`, after the roles: both must pass. Omitted, the console is open
|
|
230
270
|
// outside production and closed (403) in production.
|
|
231
271
|
const decision = cfg.admin.authorize
|
|
@@ -234,9 +274,45 @@ export const adminGuard = async (ctx, next) => {
|
|
|
234
274
|
? { allowed: false, status: 403 }
|
|
235
275
|
: { allowed: true };
|
|
236
276
|
if (!decision.allowed)
|
|
237
|
-
return
|
|
238
|
-
return
|
|
239
|
-
}
|
|
277
|
+
return { kind: 'denied', decision };
|
|
278
|
+
return { kind: 'allowed' };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The admin console gate as the console switcher reads it: `{ allowed }`, never a response, never a
|
|
282
|
+
* throw. Used as the `authorize` of the admin console's `'agora.consoles'` entry.
|
|
283
|
+
*/
|
|
284
|
+
export async function adminConsoleDecision(ctx) {
|
|
285
|
+
try {
|
|
286
|
+
const access = await evaluateAdminAccess(ctx, { probe: true });
|
|
287
|
+
if (access.kind === 'allowed')
|
|
288
|
+
return { allowed: true };
|
|
289
|
+
const unauthenticated = access.kind === 'signed-out' ||
|
|
290
|
+
access.kind === 'idle' ||
|
|
291
|
+
(access.kind === 'denied' && access.decision.status === 401);
|
|
292
|
+
return { allowed: false, reason: unauthenticated ? 'unauthenticated' : 'forbidden' };
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return { allowed: false, reason: 'forbidden' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Registers the admin console in the shared `'agora.consoles'` registry, so every other
|
|
300
|
+
* `@adonis-agora` console's switcher can link to it. A no-op when the provider did not register the
|
|
301
|
+
* app (tests that mount routes on a bare router).
|
|
302
|
+
*/
|
|
303
|
+
function announceAdminConsole(adminPrefix) {
|
|
304
|
+
let app;
|
|
305
|
+
try {
|
|
306
|
+
app = getBootedApp();
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
void (async () => {
|
|
312
|
+
const logger = (await app.container.make('logger').catch(() => undefined));
|
|
313
|
+
await registerAgoraConsole(app, adminConsoleEntry(adminPrefix, adminConsoleDecision), logger);
|
|
314
|
+
})().catch(() => { });
|
|
315
|
+
}
|
|
240
316
|
/** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
|
|
241
317
|
function consoleDenied(ctx, decision) {
|
|
242
318
|
const json = wantsJson(ctx);
|
|
@@ -286,6 +362,7 @@ const C = {
|
|
|
286
362
|
// Console React JSON API (session-authed, under {prefix}/api/*).
|
|
287
363
|
consoleShell: () => import('./admin_console/admin_shell_controller.js'),
|
|
288
364
|
consoleOverview: () => import('./admin_console/console_overview_controller.js'),
|
|
365
|
+
consoleSwitcher: () => import('./admin_console/console_switcher_controller.js'),
|
|
289
366
|
consoleUsers: () => import('./admin_console/console_users_controller.js'),
|
|
290
367
|
consoleSessions: () => import('./admin_console/console_sessions_controller.js'),
|
|
291
368
|
consoleClients: () => import('./admin_console/console_clients_controller.js'),
|
|
@@ -936,6 +1013,8 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
936
1013
|
resolvedAdminPrefix = ap;
|
|
937
1014
|
// Persiste no singleton de processo para que controllers e views usem o mesmo prefixo.
|
|
938
1015
|
setAdminPrefix(ap);
|
|
1016
|
+
// Announce the console to the other `@adonis-agora` consoles (the switcher).
|
|
1017
|
+
announceAdminConsole(ap);
|
|
939
1018
|
router
|
|
940
1019
|
.group(() => {
|
|
941
1020
|
// Console admin (SPA React self-contained): todas as rotas GET servem o
|
|
@@ -949,6 +1028,10 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
949
1028
|
// ─── JSON API do console (session-authed via adminGuard upstream) ─────
|
|
950
1029
|
// GET {ap}/api/overview
|
|
951
1030
|
router.get(`${ap}/api/overview`, [C.consoleOverview, 'handle']);
|
|
1031
|
+
// Console switcher: the other `@adonis-agora` consoles this viewer may open, and the
|
|
1032
|
+
// global search across them (search BEFORE the list: fixed path first).
|
|
1033
|
+
router.get(`${ap}/api/consoles/search`, [C.consoleSwitcher, 'search']);
|
|
1034
|
+
router.get(`${ap}/api/consoles`, [C.consoleSwitcher, 'index']);
|
|
952
1035
|
// Usuários.
|
|
953
1036
|
router.get(`${ap}/api/users`, [C.consoleUsers, 'index']);
|
|
954
1037
|
router.get(`${ap}/api/users/:id`, [C.consoleUsers, 'show']);
|