@iyulab/modern-app 0.44.2 → 0.45.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/CHANGELOG.md +23 -0
- package/README.md +4 -7
- package/dist/App.js +12 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/components/ActionBar.js +2 -2
- package/dist/components/EmptyState.js +2 -2
- package/dist/components/GroupBox.js +2 -2
- package/dist/components/InfoField.js +3 -7
- package/dist/components/InfoSection.js +2 -2
- package/dist/components/MasterDetailLayout.js +2 -2
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/SidebarButton.js +2 -2
- package/dist/components/SidebarGroup.js +2 -2
- package/dist/components/SidebarLink.js +2 -2
- package/dist/components/SidebarSection.js +2 -2
- package/dist/components/Timeline.js +1 -1
- package/dist/components/TimelineItem.js +2 -2
- package/dist/components/Wizard.js +2 -2
- package/dist/components/Wizard.styles.js +3 -0
- package/dist/internals/StyledElement.js +4 -7
- package/dist/layouts/SidebarLayout.js +2 -2
- package/dist/types/AppConfigs.d.ts +3 -7
- package/dist/types/AuthConfig.d.ts +32 -14
- package/package.json +3 -3
- package/skills/modern-app/references/api.md +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.45.0] - 2026-10-08
|
|
4
|
+
|
|
5
|
+
### Changed (breaking)
|
|
6
|
+
|
|
7
|
+
- **The boot auth gate reads `me()`'s answer by `status`.** `me()` returns an `AuthSession` —
|
|
8
|
+
`{ status: 'authenticated', user }`, `{ status: 'anonymous' }` or `{ status: 'unknown', error }` — the same shape
|
|
9
|
+
`@iyulab/enterprise`'s `createAuthClient().fetchMe()` returns, so the two connect as `me: () => auth.fetchMe()`.
|
|
10
|
+
Before, any value meant "signed in": a session client that answers "signed out" with an object (`fetchMe()` gives
|
|
11
|
+
`{ status: 'anonymous' }` on a 401) opened the shell to a user who had not signed in, with that object as
|
|
12
|
+
`app.user` — and `me`'s `unknown` return type let it type-check.
|
|
13
|
+
`anonymous` renders `renderLogin`; `unknown` (or `me()` throwing) renders `renderUnavailable` with the answer's
|
|
14
|
+
`error`. An answer whose `status` is not one of the three makes `app.load()` reject with a `TypeError` instead of
|
|
15
|
+
being guessed to mean signed in.
|
|
16
|
+
**Migrating:** return `{ status: 'authenticated', user }` where you returned the user and `{ status: 'anonymous' }`
|
|
17
|
+
where you returned `null`; with `@iyulab/enterprise`, drop the adapter and pass `auth.fetchMe` through.
|
|
18
|
+
|
|
19
|
+
## [0.44.3] - 2026-10-08
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `u-wizard` keeps its live announcement inside the element — the absolutely positioned announcement escaped an
|
|
24
|
+
unpositioned scroll container and could stretch the document.
|
|
25
|
+
|
|
3
26
|
## [0.44.2] - 2026-10-07
|
|
4
27
|
|
|
5
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -184,11 +184,7 @@ const auth = createAuthClient<User, Cred>({ meUrl: '/api/auth/me', loginUrl: '/a
|
|
|
184
184
|
await app.load({
|
|
185
185
|
layout: { type: 'sidebar', /* ... */ },
|
|
186
186
|
auth: {
|
|
187
|
-
me:
|
|
188
|
-
const s = await auth.fetchMe();
|
|
189
|
-
if (s.status === 'unknown') throw s.error; // 모름(서버 다운·오프라인) → renderUnavailable
|
|
190
|
-
return s.status === 'authenticated' ? s.user : null; // null → 미인증 → renderLogin
|
|
191
|
-
},
|
|
187
|
+
me: () => auth.fetchMe(), // SessionState: authenticated · anonymous → renderLogin · unknown → renderUnavailable
|
|
192
188
|
renderLogin: ({ root, onSuccess }) => renderLoginPage(root, auth, onSuccess),
|
|
193
189
|
renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
|
|
194
190
|
onAuthenticated: (user) => setPermissions((user as User).Permissions),
|
|
@@ -199,8 +195,9 @@ await app.load({
|
|
|
199
195
|
app.user; // 인증된 현재 사용자(미인증/미사용 시 undefined)
|
|
200
196
|
```
|
|
201
197
|
|
|
202
|
-
- `me()`
|
|
203
|
-
- `me()` 가
|
|
198
|
+
- `me()` 는 `AuthSession` — `{ status: 'authenticated', user }` · `{ status: 'anonymous' }` · `{ status: 'unknown', error }` — 을 돌려준다. `authenticated` 면 셸을 로드하고 `app.user` 는 `user` 다. `anonymous` 면 `renderLogin({ root, onSuccess })`.
|
|
199
|
+
- `unknown`(또는 `me()` 가 **던짐**)이면 세션을 «모름» 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable({ root, error, retry })` 를 그리고, 없으면 `app.load()` 가 그 오류로 실패한다. 서버가 잠깐 503 을 낸 것을 «미인증» 으로 다루면 로그인된 사용자가 로그인 화면으로 간다.
|
|
200
|
+
- `status` 가 셋 중 하나가 아닌 답(사용자 객체 · `null`)은 `app.load()` 가 `TypeError` 로 거절한다 — 인증으로 추측하지 않는다.
|
|
204
201
|
- 로그인 성공 시 `onSuccess()` 를 호출하면 앱이 재로드되어 셸이 나타나고 로그인 UI 는 정리된다.
|
|
205
202
|
- `auth` 미지정 시 완전히 하위호환(게이트 없이 기존대로 로드).
|
|
206
203
|
|
package/dist/App.js
CHANGED
|
@@ -52,17 +52,23 @@ var u = class s {
|
|
|
52
52
|
}, n;
|
|
53
53
|
try {
|
|
54
54
|
n = await a.auth.me();
|
|
55
|
-
} catch (
|
|
56
|
-
|
|
55
|
+
} catch (e) {
|
|
56
|
+
n = {
|
|
57
|
+
status: "unknown",
|
|
58
|
+
error: e
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (n?.status === "unknown") {
|
|
62
|
+
if (!a.auth.renderUnavailable) throw n.error;
|
|
57
63
|
let r = a.auth.renderUnavailable({
|
|
58
64
|
root: e,
|
|
59
|
-
error: n,
|
|
65
|
+
error: n.error,
|
|
60
66
|
retry: t
|
|
61
67
|
});
|
|
62
68
|
r && (this._gateTeardown = r);
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
|
-
if (n
|
|
71
|
+
if (n?.status === "anonymous") {
|
|
66
72
|
let n = a.auth.renderLogin({
|
|
67
73
|
root: e,
|
|
68
74
|
onSuccess: t
|
|
@@ -70,7 +76,8 @@ var u = class s {
|
|
|
70
76
|
n && (this._gateTeardown = n);
|
|
71
77
|
return;
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
if (n?.status !== "authenticated") throw TypeError("auth.me() must return { status: 'authenticated', user } | { status: 'anonymous' } | { status: 'unknown', error }");
|
|
80
|
+
this._user = n.user, await a.auth.onAuthenticated?.(n.user);
|
|
74
81
|
}
|
|
75
82
|
let o = a.root || document.body;
|
|
76
83
|
this._layout = await this.createLayout(o, a.layout), this._screen = new e({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
|
|
2
2
|
function e(e, t, n, r) {
|
|
3
3
|
var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
|
|
4
4
|
if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function e(e, t) {
|
|
3
3
|
if (typeof Reflect == "object" && typeof Reflect.metadata == "function") return Reflect.metadata(e, t);
|
|
4
4
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
5
|
import { styles as i } from "./ActionBar.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
5
|
import { styles as i } from "./GroupBox.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./InfoField.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -26,11 +26,7 @@ var f = class extends n {
|
|
|
26
26
|
let e = t.getAttribute("role") ?? (t.localName === "progress" ? "progressbar" : t.localName === "meter" ? "meter" : null);
|
|
27
27
|
if (e !== "progressbar" && e !== "meter") continue;
|
|
28
28
|
let n = this.namedWidgets.get(t), r = t.getAttribute("aria-label");
|
|
29
|
-
|
|
30
|
-
this.namedWidgets.delete(t);
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
this.label ? (t.setAttribute("aria-label", this.label), this.namedWidgets.set(t, this.label)) : n !== void 0 && (t.removeAttribute("aria-label"), this.namedWidgets.delete(t));
|
|
29
|
+
t.hasAttribute("aria-labelledby") || r !== null && r !== n ? this.namedWidgets.delete(t) : this.label ? (t.setAttribute("aria-label", this.label), this.namedWidgets.set(t, this.label)) : n !== void 0 && (t.removeAttribute("aria-label"), this.namedWidgets.delete(t));
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
updated(e) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./InfoSection.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { styles as i } from "./SidebarButton.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { SidebarLink as i } from "./SidebarLink.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { DEFAULT_NAV_ICON as i } from "../internals/nav-icon.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./SidebarSection.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
2
2
|
import { StyledElement as t } from "../internals/StyledElement.js";
|
|
3
3
|
import "./TimelineItem.js";
|
|
4
4
|
import { styles as n } from "./Timeline.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
5
|
import { styles as i } from "./TimelineItem.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { styles as i } from "./Wizard.styles.js";
|
|
@@ -3,6 +3,9 @@ import { css as e } from "lit";
|
|
|
3
3
|
var t = e`
|
|
4
4
|
:host {
|
|
5
5
|
display: flex;
|
|
6
|
+
/* 위치 지정 — 안의 숨김 문구(절대 위치 · 1px)의 포함 블록이 이 요소가 된다. 없으면 포함 블록이 문서 전체라 호스트의
|
|
7
|
+
overflow 감싸개를 건너뛰고 그 정적 위치에서 문서를 늘렸다(위치 지정되지 않은 스크롤 상자 안에서). */
|
|
8
|
+
position: relative;
|
|
6
9
|
flex-direction: row;
|
|
7
10
|
gap: var(--u-space-xl, 20px);
|
|
8
11
|
min-width: 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { property as n } from "lit/decorators.js";
|
|
4
4
|
import { UElement as r } from "@iyulab/components/dist/components/UElement.js";
|
|
5
5
|
//#region src/internals/StyledElement.ts
|
|
@@ -25,11 +25,8 @@ var i = class extends r {
|
|
|
25
25
|
for (let [n, r] of Object.entries(t)) {
|
|
26
26
|
if (r == null) continue;
|
|
27
27
|
let t = String(r);
|
|
28
|
-
if (n.startsWith("--") || n.includes("-"))
|
|
29
|
-
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
28
|
+
if (n.startsWith("--") || n.includes("-")) e.style.setProperty(n, t);
|
|
29
|
+
else try {
|
|
33
30
|
e.style[n] = t;
|
|
34
31
|
} catch {
|
|
35
32
|
let r = n.replace(/[A-Z]/g, (e) => "-" + e.toLowerCase());
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { app as e } from "../App.js";
|
|
2
2
|
import { filterSidebarItems as t } from "./filterSidebarItems.js";
|
|
3
|
-
import n from "../_virtual/_@oxc-project_runtime@0.
|
|
4
|
-
import r from "../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import n from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
4
|
+
import r from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
5
5
|
import { StyledElement as i } from "../internals/StyledElement.js";
|
|
6
6
|
import { getLocaleStrings as a } from "../internals/locale.js";
|
|
7
7
|
import { slotHasContent as o } from "../internals/slotted.js";
|
|
@@ -101,8 +101,8 @@ export interface AppConfig {
|
|
|
101
101
|
*/
|
|
102
102
|
i18n?: I18nInitOptions;
|
|
103
103
|
/**
|
|
104
|
-
* 부팅 인증 게이트(선택). 지정하면 앱 셸을 만들기 전에 `me()`
|
|
105
|
-
*
|
|
104
|
+
* 부팅 인증 게이트(선택). 지정하면 앱 셸을 만들기 전에 `me()` 의 답(`AuthSession`)으로 세션을 판정하여
|
|
105
|
+
* `authenticated` 면 셸 로드, `anonymous` 면 `renderLogin` 으로 로그인 UI, `unknown`(또는 `me()` 가 던짐)이면
|
|
106
106
|
* `renderUnavailable` 을 그린다.
|
|
107
107
|
* @see AuthGateConfig
|
|
108
108
|
* @example
|
|
@@ -110,11 +110,7 @@ export interface AppConfig {
|
|
|
110
110
|
* app.load({
|
|
111
111
|
* layout: { type: 'sidebar', ... },
|
|
112
112
|
* auth: {
|
|
113
|
-
* me:
|
|
114
|
-
* const s = await authClient.fetchMe();
|
|
115
|
-
* if (s.status === 'unknown') throw s.error; // 모름 — 로그인이 아니라 renderUnavailable
|
|
116
|
-
* return s.status === 'authenticated' ? s.user : null; // null → 미인증
|
|
117
|
-
* },
|
|
113
|
+
* me: () => authClient.fetchMe(), // @iyulab/enterprise createAuthClient — SessionState 그대로
|
|
118
114
|
* renderLogin: ({ root, onSuccess }) => renderLoginPage(root, onSuccess),
|
|
119
115
|
* renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
|
|
120
116
|
* onAuthenticated: (user) => setPermissions((user as User).Permissions),
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 부팅 인증 게이트 설정.
|
|
3
3
|
*
|
|
4
|
-
* `app.load({ auth })` 에 넘기면, 앱 셸(레이아웃·라우터)을 만들기 전에 세션을
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* `app.load({ auth })` 에 넘기면, 앱 셸(레이아웃·라우터)을 만들기 전에 세션을 판정한다. `me()` 는
|
|
5
|
+
* 세 답 중 하나(`AuthSession`)를 돌려준다:
|
|
6
|
+
* - `authenticated` — 셸을 로드한다(`user` 가 `app.user` 가 된다).
|
|
7
|
+
* - `anonymous` — 셸 대신 `renderLogin` 으로 로그인 UI 를 띄운다. 로그인 성공 시 `onSuccess()` 를
|
|
8
|
+
* 호출하면 앱이 (재)로드되어 셸이 나타난다.
|
|
9
|
+
* - `unknown` — 세션을 확인하지 못했다(서버 다운·오프라인). 미인증이 아니므로 로그인 UI 를 그리지
|
|
10
|
+
* 않는다 — `renderUnavailable` 이 있으면 그것을, 없으면 `load` 가 그 오류로 실패한다. `me()` 가
|
|
11
|
+
* **던지는** 것도 같은 답이다.
|
|
12
|
+
*
|
|
13
|
+
* `@iyulab/enterprise` 의 `createAuthClient().fetchMe()` 가 돌려주는 `SessionState` 가 이 형태 그대로라
|
|
14
|
+
* `me: () => auth.fetchMe()` 로 잇는다.
|
|
10
15
|
*
|
|
11
16
|
* 프레임워크는 인증의 **오케스트레이션**(판정 → 분기 → 재로드)만 소유한다. 세션 조회/로그인
|
|
12
17
|
* 자체(HTTP)와 사용자·권한 형태, 세션-중 401 처리는 앱/`@iyulab/enterprise` 가 소유한다.
|
|
@@ -14,33 +19,46 @@
|
|
|
14
19
|
export interface AuthGateContext {
|
|
15
20
|
/** 로그인 UI 를 그릴 루트 요소(= `app.load` 의 `root`, 기본 `document.body`). */
|
|
16
21
|
root: Element;
|
|
17
|
-
/** 로그인 성공 시 호출한다. 앱이 (재)로드되어 셸을 띄운다(이때 `me()` 는
|
|
22
|
+
/** 로그인 성공 시 호출한다. 앱이 (재)로드되어 셸을 띄운다(이때 `me()` 는 `authenticated` 를 돌려줘야 한다). */
|
|
18
23
|
onSuccess: () => void;
|
|
19
24
|
}
|
|
20
25
|
/** `renderUnavailable` 이 받는 맥락 — 세션을 확인하지 못했을 때. */
|
|
21
26
|
export interface AuthGateUnavailableContext {
|
|
22
27
|
/** 안내 UI 를 그릴 루트 요소(= `app.load` 의 `root`, 기본 `document.body`). */
|
|
23
28
|
root: Element;
|
|
24
|
-
/** `me()` 가 던진 값. */
|
|
29
|
+
/** `unknown` 답의 `error`, 또는 `me()` 가 던진 값. */
|
|
25
30
|
error: unknown;
|
|
26
31
|
/** 다시 확인한다 — 앱이 (재)로드되어 `me()` 를 다시 부른다(재시도 버튼·`online` 이벤트에서). */
|
|
27
32
|
retry: () => void;
|
|
28
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* 세션 조회의 답 — `status` 로 가른다. «미인증»과 «모름»은 다른 답이다: 서버가 잠깐 503 을 낸 것을
|
|
36
|
+
* 미인증으로 다루면 로그인된 사용자가 로그인 화면으로 간다.
|
|
37
|
+
*/
|
|
38
|
+
export type AuthSession<TUser = unknown> = {
|
|
39
|
+
status: 'authenticated';
|
|
40
|
+
user: TUser;
|
|
41
|
+
} | {
|
|
42
|
+
status: 'anonymous';
|
|
43
|
+
} | {
|
|
44
|
+
status: 'unknown';
|
|
45
|
+
error: unknown;
|
|
46
|
+
};
|
|
29
47
|
export interface AuthGateConfig {
|
|
30
48
|
/**
|
|
31
|
-
* 현재 세션
|
|
32
|
-
*
|
|
33
|
-
*
|
|
49
|
+
* 현재 세션 조회 — `AuthSession` 을 돌려준다(동기/비동기 모두 허용). 던지면 `unknown` 으로 본다.
|
|
50
|
+
* `status` 가 셋 중 하나가 아닌 값을 돌려주면 `load` 가 `TypeError` 로 실패한다 — 사용자 객체를 그대로
|
|
51
|
+
* 돌려주는 옛 형태를 «인증됨»으로 추측하지 않는다.
|
|
34
52
|
*/
|
|
35
|
-
me: () =>
|
|
53
|
+
me: () => AuthSession | Promise<AuthSession>;
|
|
36
54
|
/**
|
|
37
55
|
* 미인증 시 `context.root` 에 로그인 UI 를 그린다. 성공하면 `context.onSuccess()` 를 호출해야 한다.
|
|
38
56
|
* 정리 함수를 반환하면 앱 로드/`unload` 시 호출되어 로그인 UI 를 제거한다.
|
|
39
57
|
*/
|
|
40
58
|
renderLogin: (context: AuthGateContext) => (() => void) | void;
|
|
41
59
|
/**
|
|
42
|
-
* `me()` 가
|
|
43
|
-
* 확인한다. 정리 함수를 반환하면 앱 로드/`unload` 시 호출된다. 없으면 `load` 가
|
|
60
|
+
* 세션을 모를 때(`unknown` · `me()` 가 던짐) `context.root` 에 안내 UI 를 그린다(선택). `context.retry()` 로
|
|
61
|
+
* 다시 확인한다. 정리 함수를 반환하면 앱 로드/`unload` 시 호출된다. 없으면 `load` 가 그 오류로 실패한다.
|
|
44
62
|
*/
|
|
45
63
|
renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
|
|
46
64
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/modern-app",
|
|
3
3
|
"description": "web-framework by iyulab based on lit-element",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.45.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"@iyulab/components": ">=2.8.0",
|
|
82
82
|
"@lit/react": "^1.0.8",
|
|
83
83
|
"@types/node": "^26.1.1",
|
|
84
|
-
"@types/react": "^19.
|
|
85
|
-
"@types/react-dom": "^19.
|
|
84
|
+
"@types/react": "^19.3.0",
|
|
85
|
+
"@types/react-dom": "^19.3.0",
|
|
86
86
|
"@vitest/browser-playwright": "^5.0.0",
|
|
87
87
|
"glob": "^13.0.6",
|
|
88
88
|
"happy-dom": "^20.10.6",
|
|
@@ -227,13 +227,23 @@ The framework owns only the orchestration (check → branch → reload) — sess
|
|
|
227
227
|
`@iyulab/enterprise`'s `createAuthClient`/`createODataService`).
|
|
228
228
|
|
|
229
229
|
```typescript
|
|
230
|
+
/**
|
|
231
|
+
* A session lookup's answer. "Signed out" and "could not tell" are different answers — treating a
|
|
232
|
+
* brief 503 as signed out sends a signed-in user to the login screen. `@iyulab/enterprise`'s
|
|
233
|
+
* `createAuthClient().fetchMe()` returns exactly this shape: `me: () => auth.fetchMe()`.
|
|
234
|
+
*/
|
|
235
|
+
type AuthSession<TUser = unknown> =
|
|
236
|
+
| { status: 'authenticated'; user: TUser } // shell loads; app.user = user
|
|
237
|
+
| { status: 'anonymous' } // renderLogin
|
|
238
|
+
| { status: 'unknown'; error: unknown }; // renderUnavailable
|
|
239
|
+
|
|
230
240
|
interface AuthGateConfig {
|
|
231
241
|
/**
|
|
232
|
-
* Resolve the current session
|
|
233
|
-
*
|
|
234
|
-
*
|
|
242
|
+
* Resolve the current session — sync or async. Throwing counts as `unknown`. An answer whose
|
|
243
|
+
* `status` is not one of the three makes `app.load()` reject with a `TypeError`: a user object
|
|
244
|
+
* or `null` is never guessed to mean "signed in".
|
|
235
245
|
*/
|
|
236
|
-
me: () =>
|
|
246
|
+
me: () => AuthSession | Promise<AuthSession>;
|
|
237
247
|
|
|
238
248
|
/**
|
|
239
249
|
* Renders login UI into `context.root` when unauthenticated. Call `context.onSuccess()` on
|
|
@@ -242,10 +252,11 @@ interface AuthGateConfig {
|
|
|
242
252
|
renderLogin: (context: AuthGateContext) => (() => void) | void;
|
|
243
253
|
|
|
244
254
|
/**
|
|
245
|
-
* Renders a "can't reach the server" view into `context.root` when `
|
|
255
|
+
* Renders a "can't reach the server" view into `context.root` when the session is `unknown`
|
|
256
|
+
* (or `me()` throws). Call
|
|
246
257
|
* `context.retry()` to check again (a retry button, the `online` event). Return a cleanup
|
|
247
258
|
* function to have it called on app load/`unload`. Without it, `app.load()` rejects with
|
|
248
|
-
*
|
|
259
|
+
* that error.
|
|
249
260
|
*/
|
|
250
261
|
renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
|
|
251
262
|
|
|
@@ -263,7 +274,7 @@ interface AuthGateContext {
|
|
|
263
274
|
interface AuthGateUnavailableContext {
|
|
264
275
|
/** Root element to render into (same as `AppConfig.root`, default `document.body`). */
|
|
265
276
|
root: Element;
|
|
266
|
-
/**
|
|
277
|
+
/** The `unknown` answer's `error`, or what `me()` threw. */
|
|
267
278
|
error: unknown;
|
|
268
279
|
/** Check again — the app (re)loads and calls `me()` again. */
|
|
269
280
|
retry: () => void;
|