@infoteam/account-react 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +158 -158
  3. package/package.json +3 -3
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 GSA Infoteam
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GSA Infoteam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,158 +1,158 @@
1
- # @infoteam/account-react
2
-
3
- 인포팀 계정(IdP) 로그인을 React 앱에 붙인다. [`@infoteam/account-web`](../web)의 export를 모두 다시 내보내므로 이 패키지 하나만 설치하면 된다.
4
-
5
- 서비스 백엔드는 [세션 프로토콜](../../README.md#세션-프로토콜)의 로그인, 갱신, 로그아웃 endpoint를 구현해야 한다. 이 패키지는 그 endpoint를 호출한다. 형식이 다른 백엔드는 [`session` 옵션](../web/README.md#백엔드-형식이-다를-때)으로 붙인다.
6
-
7
- ## 설치
8
-
9
- ```bash
10
- pnpm add @infoteam/account-react zod
11
- ```
12
-
13
- `zod`(4.x)는 peerDependency다. SDK가 export 하는 스키마를 앱과 같은 인스턴스로 쓰기 위해서다.
14
- `openapi-fetch`나 `axios` 어댑터를 쓸 때만 그 라이브러리를 설치한다.
15
-
16
- ## 사용
17
-
18
- ### 1. 클라이언트 생성
19
-
20
- ```ts
21
- // src/account.ts
22
- import { createAccountClient } from '@infoteam/account-react';
23
-
24
- export const account = createAccountClient({
25
- issuer: 'production',
26
- clientId: import.meta.env.VITE_ACCOUNT_CLIENT_ID,
27
- api: import.meta.env.VITE_API_URL,
28
- });
29
- ```
30
-
31
- | 옵션 | 설명 |
32
- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
33
- | `issuer` | `'production'` 또는 `'staging'` |
34
- | `clientId` | IdP에 등록한 client id |
35
- | `api` | 서비스 백엔드 주소. 경로가 기본값과 다르면 `{ baseUrl, paths: { refresh: '/auth/refresh-token' } }`로 준다 |
36
- | `scopes` | 생략하면 IdP에 등록한 필수 scope와 선택 scope를 모두 요청한다. `openid`는 항상 함께 요청한다 |
37
- | `redirectUri` | 생략하면 `/auth/callback`이다. IdP에 이 URL을 등록만 하고, 라우트는 만들지 않는다. SDK가 라우터보다 먼저 처리한다 |
38
- | `silentLogin` | 자동 로그인. 아래 설명 참고 |
39
-
40
- `silentLogin: true`이면 서비스 세션이 없을 때 화면 없이 IdP에 한 번 다녀온다. 사용자가 다른 인포팀 서비스에서 이미 IdP에 로그인했다면 로그인 버튼을
41
- 누르지 않아도 로그인되고, 아니면 에러 없이 비로그인 상태로 돌아온다. 탭마다 한 번만 시도하고, 로그아웃한 뒤에는 시도하지 않는다.
42
-
43
- ### 2. Provider
44
-
45
- ```tsx
46
- <AccountProvider client={account} fallback={<Splash />}>
47
- <RouterProvider router={router} />
48
- </AccountProvider>
49
- ```
50
-
51
- - client는 라우터보다 먼저 만들어야 한다. client가 callback 주소를 라우터보다 먼저 처리해서 로그인 전 위치로 바꾸기 때문이다. 위처럼 모듈 최상단에서 만들고 라우트에서 import 하면 된다.
52
- - `fallback`은 IdP에서 돌아온 로그인을 마무리하거나 자동 로그인을 시도하는 동안에만 보인다. 그 밖에는 라우터를 바로 그린다.
53
-
54
- ### 3. 로그인이 필요한 라우트
55
-
56
- 라우터의 guard에서 `requireAuth`를 호출한다. 세션이 있으면 그대로 통과하고, 없으면 사용자를 IdP로 보냈다가 원래 가려던 페이지로 돌아오게 한다.
57
- `returnTo`에는 라우터가 주는 이동 대상을 넘긴다. 생략하면 현재 주소를 쓰는데, 페이지를 이동하는 중에는 현재 주소가 이전 페이지이기 때문이다.
58
-
59
- **TanStack Router** (파일 기반이든 `createRoute`든 같다)
60
-
61
- ```ts
62
- // routes/_auth-required/route.tsx
63
- export const Route = createFileRoute('/_auth-required')({
64
- beforeLoad: ({ location }) => account.requireAuth({ returnTo: location.href }),
65
- errorComponent: SignInFailed, // callback 이 실패하면 requireAuth 가 그 에러를 던진다
66
- });
67
-
68
- // router.ts: 로그아웃이나 세션 만료 뒤 guard 를 다시 돌린다
69
- account.store.subscribe(() => router.invalidate());
70
- ```
71
-
72
- **React Router** (data router, v6.4 이상)
73
-
74
- ```ts
75
- const router = createBrowserRouter([
76
- {
77
- loader: ({ request }) => account.requireAuth({ returnTo: request.url }),
78
- errorElement: <SignInFailed />,
79
- children: [{ path: '/admin', element: <Admin /> }],
80
- },
81
- ]);
82
- account.store.subscribe(() => router.revalidate());
83
- ```
84
-
85
- 7.9 이상에서 middleware를 켰다면 같은 호출을 client middleware에 둬도 된다.
86
-
87
- **`<Routes>` 컴포넌트 방식이나 라우터가 없는 앱**
88
-
89
- ```tsx
90
- <RequireAuth fallback={<Loading />}>
91
- <MyPage />
92
- </RequireAuth>
93
- ```
94
-
95
- 로그인 화면을 따로 두는 앱은 `redirect={false}`를 넘기고 `fallback`에 로그인 화면을 둔다.
96
-
97
- - `requireAuth`는 사용자를 IdP로 보내는 동안 끝나지 않는 Promise를 돌려준다. 그동안 라우터는 이동 중 화면을 유지한다.
98
- - 로그인이 실패했으면(사용자가 취소했거나 백엔드가 거절했으면) 다시 IdP로 보내지 않고 그 에러를 던진다. 다시 보내면 취소한 사용자가 빠져나갈 수
99
- 없기 때문이다. 에러 화면에서 `error.code`로 경우를 나누고, 다시 시도하는 버튼에서 `login()`을 호출한다.
100
- - 다른 origin의 `returnTo`는 버린다.
101
-
102
- ### 4. 상태와 로그인
103
-
104
- ```tsx
105
- const { status, error, login, logout, retryLogin, fetch } = useAccount();
106
-
107
- if (status === 'unauthenticated') return <button onClick={() => login()}>로그인</button>;
108
- ```
109
-
110
- `status`는 `loading`, `authenticated`, `unauthenticated` 중 하나다. IdP에서 돌아온 로그인이나 세션 복구가 실패하면 `error`에 `AccountError`가 들어간다. `login()` 호출 자체가 실패하면(예: IdP에 연결하지 못함) 그 Promise가 reject 되므로 버튼에서도 에러를 처리한다. 자주 보는 `error.code`는 다음과 같다.
111
-
112
- | `error.code` | 뜻 |
113
- | ----------------------- | ----------------------------------------------------------------------------- |
114
- | `user_cancelled` | 사용자가 IdP 화면에서 취소했다 |
115
- | `verification_required` | 서비스가 필수로 요구한 정보(예: 학번)를 사용자가 IdP에서 아직 인증하지 않았다 |
116
- | `session_rejected` | 서비스 백엔드가 403으로 로그인을 거절했다. 거절 이유는 `error.details`에 있다 |
117
- | `server_error` | 서비스 백엔드가 5xx로 응답했거나, 성공 응답에 `access_token`이 없었다 |
118
- | `network_error` | IdP나 서비스 백엔드에 연결하지 못했다 |
119
-
120
- `session_rejected`는 약관 동의처럼 로그인 전에 추가 입력이 필요할 때 쓴다. `canRetryLogin`이 `true`이면(거절 응답에 ticket이 있으면) 사용자에게 입력을 받은 뒤
121
- `retryLogin(body)`로 보내 IdP를 다시 거치지 않고 로그인을 마친다. 백엔드 쪽은 [세션 프로토콜](../../README.md#세션-프로토콜)의 거절과 ticket 규칙을 참고한다.
122
-
123
- ### 5. API 요청
124
-
125
- `fetch`는 access token을 붙여 요청하고, 401을 받으면 토큰을 한 번 갱신한 뒤 다시 보낸다. 여러 요청이 동시에 401을 받으면 먼저 성공한 갱신의 토큰을 함께 쓴다.
126
- access token은 메모리에만 두고, refresh token은 서비스 백엔드가 httpOnly 쿠키로 관리한다. 페이지를 새로 고치면 refresh로 세션을 복구한다.
127
-
128
- `fetch`는 전역 `fetch`와 사용법이 같다. 주소와 상관없이 access token을 붙이므로 서비스 백엔드에 보내는 요청에만 쓴다. 상대 경로는 `api` 기준이 아니라 현재 페이지 기준이므로 백엔드 주소를 붙여 쓴다.
129
-
130
- ```ts
131
- const response = await account.fetch(`${API_URL}/me`); // 컴포넌트 안에서는 useAccount().fetch
132
- ```
133
-
134
- openapi-fetch를 쓰면 다음과 같다.
135
-
136
- ```ts
137
- import { createAccountMiddleware } from '@infoteam/account-react/openapi-fetch';
138
-
139
- api.use(createAccountMiddleware(account));
140
- ```
141
-
142
- axios를 쓰면 다음과 같다.
143
-
144
- ```ts
145
- import { attachAccount } from '@infoteam/account-react/axios';
146
-
147
- attachAccount(instance, account);
148
- ```
149
-
150
- ### 6. 로그아웃
151
-
152
- ```ts
153
- await logout(); // 서비스 세션만 끝낸다
154
- await logout({ idp: true, returnTo: '/' }); // IdP 로그인 세션까지 끝낸다
155
- ```
156
-
157
- 둘의 차이는 [로그아웃](../../README.md#로그아웃)에 있다. `idp: true`이면 IdP의 로그아웃 페이지에 다녀오는데, 돌아올 때 IdP에 등록한 callback URL을
158
- 거치므로 따로 등록할 URL은 없다. IdP가 아직 로그아웃 endpoint를 제공하지 않으면 서비스 세션만 끝낸다.
1
+ # @infoteam/account-react
2
+
3
+ 인포팀 계정(IdP) 로그인을 React 앱에 붙인다. [`@infoteam/account-web`](../web)의 export를 모두 다시 내보내므로 이 패키지 하나만 설치하면 된다.
4
+
5
+ 서비스 백엔드는 [세션 프로토콜](../../README.md#세션-프로토콜)의 로그인, 갱신, 로그아웃 endpoint를 구현해야 한다. 이 패키지는 그 endpoint를 호출한다. 형식이 다른 백엔드는 [`session` 옵션](../web/README.md#백엔드-형식이-다를-때)으로 붙인다.
6
+
7
+ ## 설치
8
+
9
+ ```bash
10
+ pnpm add @infoteam/account-react zod
11
+ ```
12
+
13
+ `zod`(4.x)는 peerDependency다. SDK가 export 하는 스키마를 앱과 같은 인스턴스로 쓰기 위해서다.
14
+ `openapi-fetch`나 `axios` 어댑터를 쓸 때만 그 라이브러리를 설치한다.
15
+
16
+ ## 사용
17
+
18
+ ### 1. 클라이언트 생성
19
+
20
+ ```ts
21
+ // src/account.ts
22
+ import { createAccountClient } from '@infoteam/account-react';
23
+
24
+ export const account = createAccountClient({
25
+ issuer: 'production',
26
+ clientId: import.meta.env.VITE_ACCOUNT_CLIENT_ID,
27
+ api: import.meta.env.VITE_API_URL,
28
+ });
29
+ ```
30
+
31
+ | 옵션 | 설명 |
32
+ | ------------- | ----------------------------------------------------------------------------------------------------------------- |
33
+ | `issuer` | `'production'` 또는 `'staging'` |
34
+ | `clientId` | IdP에 등록한 client id |
35
+ | `api` | 서비스 백엔드 주소. 경로가 기본값과 다르면 `{ baseUrl, paths: { refresh: '/auth/refresh-token' } }`로 준다 |
36
+ | `scopes` | 생략하면 IdP에 등록한 필수 scope와 선택 scope를 모두 요청한다. `openid`는 항상 함께 요청한다 |
37
+ | `redirectUri` | 생략하면 `/auth/callback`이다. IdP에 이 URL을 등록만 하고, 라우트는 만들지 않는다. SDK가 라우터보다 먼저 처리한다 |
38
+ | `silentLogin` | 자동 로그인. 아래 설명 참고 |
39
+
40
+ `silentLogin: true`이면 서비스 세션이 없을 때 화면 없이 IdP에 한 번 다녀온다. 사용자가 다른 인포팀 서비스에서 이미 IdP에 로그인했다면 로그인 버튼을
41
+ 누르지 않아도 로그인되고, 아니면 에러 없이 비로그인 상태로 돌아온다. 탭마다 한 번만 시도하고, 로그아웃한 뒤에는 시도하지 않는다.
42
+
43
+ ### 2. Provider
44
+
45
+ ```tsx
46
+ <AccountProvider client={account} fallback={<Splash />}>
47
+ <RouterProvider router={router} />
48
+ </AccountProvider>
49
+ ```
50
+
51
+ - client는 라우터보다 먼저 만들어야 한다. client가 callback 주소를 라우터보다 먼저 처리해서 로그인 전 위치로 바꾸기 때문이다. 위처럼 모듈 최상단에서 만들고 라우트에서 import 하면 된다.
52
+ - `fallback`은 IdP에서 돌아온 로그인을 마무리하거나 자동 로그인을 시도하는 동안에만 보인다. 그 밖에는 라우터를 바로 그린다.
53
+
54
+ ### 3. 로그인이 필요한 라우트
55
+
56
+ 라우터의 guard에서 `requireAuth`를 호출한다. 세션이 있으면 그대로 통과하고, 없으면 사용자를 IdP로 보냈다가 원래 가려던 페이지로 돌아오게 한다.
57
+ `returnTo`에는 라우터가 주는 이동 대상을 넘긴다. 생략하면 현재 주소를 쓰는데, 페이지를 이동하는 중에는 현재 주소가 이전 페이지이기 때문이다.
58
+
59
+ **TanStack Router** (파일 기반이든 `createRoute`든 같다)
60
+
61
+ ```ts
62
+ // routes/_auth-required/route.tsx
63
+ export const Route = createFileRoute('/_auth-required')({
64
+ beforeLoad: ({ location }) => account.requireAuth({ returnTo: location.href }),
65
+ errorComponent: SignInFailed, // callback 이 실패하면 requireAuth 가 그 에러를 던진다
66
+ });
67
+
68
+ // router.ts: 로그아웃이나 세션 만료 뒤 guard 를 다시 돌린다
69
+ account.store.subscribe(() => router.invalidate());
70
+ ```
71
+
72
+ **React Router** (data router, v6.4 이상)
73
+
74
+ ```ts
75
+ const router = createBrowserRouter([
76
+ {
77
+ loader: ({ request }) => account.requireAuth({ returnTo: request.url }),
78
+ errorElement: <SignInFailed />,
79
+ children: [{ path: '/admin', element: <Admin /> }],
80
+ },
81
+ ]);
82
+ account.store.subscribe(() => router.revalidate());
83
+ ```
84
+
85
+ 7.9 이상에서 middleware를 켰다면 같은 호출을 client middleware에 둬도 된다.
86
+
87
+ **`<Routes>` 컴포넌트 방식이나 라우터가 없는 앱**
88
+
89
+ ```tsx
90
+ <RequireAuth fallback={<Loading />}>
91
+ <MyPage />
92
+ </RequireAuth>
93
+ ```
94
+
95
+ 로그인 화면을 따로 두는 앱은 `redirect={false}`를 넘기고 `fallback`에 로그인 화면을 둔다.
96
+
97
+ - `requireAuth`는 사용자를 IdP로 보내는 동안 끝나지 않는 Promise를 돌려준다. 그동안 라우터는 이동 중 화면을 유지한다.
98
+ - 로그인이 실패했으면(사용자가 취소했거나 백엔드가 거절했으면) 다시 IdP로 보내지 않고 그 에러를 던진다. 다시 보내면 취소한 사용자가 빠져나갈 수
99
+ 없기 때문이다. 에러 화면에서 `error.code`로 경우를 나누고, 다시 시도하는 버튼에서 `login()`을 호출한다.
100
+ - 다른 origin의 `returnTo`는 버린다.
101
+
102
+ ### 4. 상태와 로그인
103
+
104
+ ```tsx
105
+ const { status, error, login, logout, retryLogin, fetch } = useAccount();
106
+
107
+ if (status === 'unauthenticated') return <button onClick={() => login()}>로그인</button>;
108
+ ```
109
+
110
+ `status`는 `loading`, `authenticated`, `unauthenticated` 중 하나다. IdP에서 돌아온 로그인이나 세션 복구가 실패하면 `error`에 `AccountError`가 들어간다. `login()` 호출 자체가 실패하면(예: IdP에 연결하지 못함) 그 Promise가 reject 되므로 버튼에서도 에러를 처리한다. 자주 보는 `error.code`는 다음과 같다.
111
+
112
+ | `error.code` | 뜻 |
113
+ | ----------------------- | ----------------------------------------------------------------------------- |
114
+ | `user_cancelled` | 사용자가 IdP 화면에서 취소했다 |
115
+ | `verification_required` | 서비스가 필수로 요구한 정보(예: 학번)를 사용자가 IdP에서 아직 인증하지 않았다 |
116
+ | `session_rejected` | 서비스 백엔드가 403으로 로그인을 거절했다. 거절 이유는 `error.details`에 있다 |
117
+ | `server_error` | 서비스 백엔드가 5xx로 응답했거나, 성공 응답에 `access_token`이 없었다 |
118
+ | `network_error` | IdP나 서비스 백엔드에 연결하지 못했다 |
119
+
120
+ `session_rejected`는 약관 동의처럼 로그인 전에 추가 입력이 필요할 때 쓴다. `canRetryLogin`이 `true`이면(거절 응답에 ticket이 있으면) 사용자에게 입력을 받은 뒤
121
+ `retryLogin(body)`로 보내 IdP를 다시 거치지 않고 로그인을 마친다. 백엔드 쪽은 [세션 프로토콜](../../README.md#세션-프로토콜)의 거절과 ticket 규칙을 참고한다.
122
+
123
+ ### 5. API 요청
124
+
125
+ `fetch`는 access token을 붙여 요청하고, 401을 받으면 토큰을 한 번 갱신한 뒤 다시 보낸다. 여러 요청이 동시에 401을 받으면 먼저 성공한 갱신의 토큰을 함께 쓴다.
126
+ access token은 메모리에만 두고, refresh token은 서비스 백엔드가 httpOnly 쿠키로 관리한다. 페이지를 새로 고치면 refresh로 세션을 복구한다.
127
+
128
+ `fetch`는 전역 `fetch`와 사용법이 같다. 주소와 상관없이 access token을 붙이므로 서비스 백엔드에 보내는 요청에만 쓴다. 상대 경로는 `api` 기준이 아니라 현재 페이지 기준이므로 백엔드 주소를 붙여 쓴다.
129
+
130
+ ```ts
131
+ const response = await account.fetch(`${API_URL}/me`); // 컴포넌트 안에서는 useAccount().fetch
132
+ ```
133
+
134
+ openapi-fetch를 쓰면 다음과 같다.
135
+
136
+ ```ts
137
+ import { createAccountMiddleware } from '@infoteam/account-react/openapi-fetch';
138
+
139
+ api.use(createAccountMiddleware(account));
140
+ ```
141
+
142
+ axios를 쓰면 다음과 같다.
143
+
144
+ ```ts
145
+ import { attachAccount } from '@infoteam/account-react/axios';
146
+
147
+ attachAccount(instance, account);
148
+ ```
149
+
150
+ ### 6. 로그아웃
151
+
152
+ ```ts
153
+ await logout(); // 서비스 세션만 끝낸다
154
+ await logout({ idp: true, returnTo: '/' }); // IdP 로그인 세션까지 끝낸다
155
+ ```
156
+
157
+ 둘의 차이는 [로그아웃](../../README.md#로그아웃)에 있다. `idp: true`이면 IdP의 로그아웃 페이지에 다녀오는데, 돌아올 때 IdP에 등록한 callback URL을
158
+ 거치므로 따로 등록할 URL은 없다. IdP가 아직 로그아웃 endpoint를 제공하지 않으면 서비스 세션만 끝낸다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infoteam/account-react",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "React bindings for the Infoteam account IdP browser client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "./package.json": "./package.json"
24
24
  },
25
25
  "dependencies": {
26
- "@infoteam/account-web": "^0.6.0",
26
+ "@infoteam/account-web": "^0.7.0",
27
27
  "zustand": "^5.0.15"
28
28
  },
29
29
  "peerDependencies": {
@@ -32,7 +32,7 @@
32
32
  "openapi-fetch": ">=0.13.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@infoteam/account-core": "^0.6.0",
35
+ "@infoteam/account-core": "^0.7.0",
36
36
  "@testing-library/react": "^16.3.3",
37
37
  "@types/react": "^19.3.0",
38
38
  "@types/react-dom": "^19.3.0",