@ph-cms/client-sdk 0.1.27 → 0.1.30

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/README.md CHANGED
@@ -1,14 +1,15 @@
1
- # @ph-cms/client-sdk 0.1.27
1
+ # @ph-cms/client-sdk 0.1.28
2
2
 
3
3
  PH-CMS 클라이언트 SDK — 브라우저 및 React 애플리케이션을 위한 통합 클라이언트.
4
4
 
5
5
  **주요 기능:**
6
6
 
7
7
  - 타입이 지정된 API 클라이언트
8
- - Auth Provider 인터페이스 및 구현체 (`LocalAuthProvider`, `FirebaseAuthProvider`)
8
+ - Auth Provider 인터페이스 및 구현체 (`LocalAuthProvider`, `FirebaseAuthProvider`, `StaticAuthProvider`)
9
9
  - React Context 및 Hooks
10
10
  - React Query 통합
11
11
  - 자동 토큰 갱신 (Proactive + Reactive)
12
+ - SSR (Server-Side Rendering) 지원 및 Cookie 연동 가능
12
13
  - Firebase ↔ PH-CMS 인증 동기화
13
14
  - 세분화된 인증 상태 관리 (`authStatus`)
14
15
 
@@ -206,6 +207,74 @@ API Request → 401 Unauthorized
206
207
 
207
208
  ---
208
209
 
210
+ ## Server-Side Rendering (SSR)
211
+
212
+ SDK는 SSR 환경(Next.js, Remix 등)에서 인증된 상태로 API를 호출할 수 있는 기능을 제공합니다.
213
+ 브라우저의 쿠키를 서버가 읽어 SDK에 직접 주입하는 방식으로 작동합니다.
214
+
215
+ ### 1. `StaticAuthProvider`
216
+
217
+ SSR 환경에서는 `localStorage`가 없으므로, 메모리에만 토큰을 들고 있는 `StaticAuthProvider`를 사용하거나 `PHCMSClient` 설정에 직접 토큰을 주입합니다.
218
+
219
+ ```ts
220
+ import { PHCMSClient } from '@ph-cms/client-sdk/core';
221
+
222
+ // Next.js Server Component 또는 API Route 예시
223
+ export async function getServerSideProps({ req }) {
224
+ // .env 등에 설정된 프리픽스를 가져옵니다 (기본값 phcms_)
225
+ const prefix = process.env.NEXT_PUBLIC_PH_CMS_AUTH_STORAGE_PREFIX || "phcms_";
226
+
227
+ const accessToken = req.cookies[`${prefix}access_token`];
228
+ const refreshToken = req.cookies[`${prefix}refresh_token`];
229
+
230
+ const client = new PHCMSClient({
231
+ baseURL: process.env.API_URL,
232
+ accessToken, // 쿠키에서 뽑은 토큰 주입
233
+ refreshToken,
234
+ });
235
+
236
+ // 이제 이 호출은 인증된 상태(Bearer 헤더 포함)로 서버에서 실행됩니다.
237
+ const content = await client.content.get(uid);
238
+
239
+ return { props: { content } };
240
+ }
241
+ ```
242
+
243
+ ### 2. Next.js Middleware 연동 (추천)
244
+
245
+ `httpOnly` 쿠키를 사용할 경우, 브라우저의 SDK가 리프레시 토큰에 접근할 수 없습니다. 이때 Next.js 미들웨어를 통해 토큰 갱신을 처리하면 매끄러운 UX를 제공할 수 있습니다.
246
+
247
+ ```ts
248
+ // middleware.ts (Next.js)
249
+ import { NextResponse, NextRequest } from 'next/server';
250
+
251
+ export async function middleware(req: NextRequest) {
252
+ const accessToken = req.cookies.get('access_token')?.value;
253
+ const refreshToken = req.cookies.get('refresh_token')?.value;
254
+
255
+ // 액세스 토큰은 만료되었으나 리프레시 토큰이 있는 경우
256
+ if (!accessToken && refreshToken) {
257
+ const res = await fetch(`${process.env.API_URL}/auth/refresh`, {
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/json' },
260
+ body: JSON.stringify({ refreshToken }),
261
+ });
262
+
263
+ if (res.ok) {
264
+ const tokens = await res.json();
265
+ const response = NextResponse.next();
266
+ response.cookies.set('access_token', tokens.accessToken, { httpOnly: true, path: '/' });
267
+ response.cookies.set('refresh_token', tokens.refreshToken, { httpOnly: true, path: '/' });
268
+ return response;
269
+ }
270
+ }
271
+
272
+ return NextResponse.next();
273
+ }
274
+ ```
275
+
276
+ ---
277
+
209
278
  ## Auth Providers
210
279
 
211
280
  모든 Provider는 `BaseAuthProvider` 추상 클래스를 상속합니다.
@@ -0,0 +1,22 @@
1
+ import { AuthProvider } from './interfaces';
2
+ /**
3
+ * A read-only AuthProvider for SSR or static environments.
4
+ * It holds tokens in memory and does not attempt automatic refresh
5
+ * or storage persistence.
6
+ */
7
+ export declare class StaticAuthProvider implements AuthProvider {
8
+ private accessToken;
9
+ private refreshToken;
10
+ readonly type: "LOCAL";
11
+ constructor(accessToken: string | null, refreshToken?: string | null);
12
+ getToken(): Promise<string | null>;
13
+ hasToken(): boolean;
14
+ getRefreshToken(): string | null;
15
+ setRefreshFn(_fn: (refreshToken: string) => Promise<{
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ }>): void;
19
+ setTokens(accessToken: string, refreshToken: string): void;
20
+ onTokenExpired(_callback: () => Promise<void>): void;
21
+ logout(): Promise<void>;
22
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StaticAuthProvider = void 0;
4
+ /**
5
+ * A read-only AuthProvider for SSR or static environments.
6
+ * It holds tokens in memory and does not attempt automatic refresh
7
+ * or storage persistence.
8
+ */
9
+ class StaticAuthProvider {
10
+ constructor(accessToken, refreshToken = null) {
11
+ this.accessToken = accessToken;
12
+ this.refreshToken = refreshToken;
13
+ this.type = 'LOCAL';
14
+ }
15
+ async getToken() {
16
+ return this.accessToken;
17
+ }
18
+ hasToken() {
19
+ return !!this.accessToken || !!this.refreshToken;
20
+ }
21
+ getRefreshToken() {
22
+ return this.refreshToken;
23
+ }
24
+ setRefreshFn(_fn) {
25
+ // No-op: Static provider doesn't refresh automatically
26
+ }
27
+ setTokens(accessToken, refreshToken) {
28
+ this.accessToken = accessToken;
29
+ this.refreshToken = refreshToken;
30
+ }
31
+ onTokenExpired(_callback) {
32
+ // No-op
33
+ }
34
+ async logout() {
35
+ this.accessToken = null;
36
+ this.refreshToken = null;
37
+ }
38
+ }
39
+ exports.StaticAuthProvider = StaticAuthProvider;
package/dist/client.d.ts CHANGED
@@ -10,6 +10,10 @@ export interface PHCMSClientConfig {
10
10
  baseURL: string;
11
11
  apiPrefix?: string;
12
12
  auth?: AuthProvider;
13
+ /** SSR 전용: 쿠키 등에서 뽑아낸 액세스 토큰을 직접 주입할 때 사용 */
14
+ accessToken?: string;
15
+ /** SSR 전용: 쿠키 등에서 뽑아낸 리프레시 토큰을 직접 주입할 때 사용 */
16
+ refreshToken?: string;
13
17
  timeout?: number;
14
18
  axiosInstance?: AxiosInstance;
15
19
  }
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PHCMSClient = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
+ const static_provider_1 = require("./auth/static-provider");
8
9
  const errors_1 = require("./errors");
9
10
  const auth_1 = require("./modules/auth");
10
11
  const channel_1 = require("./modules/channel");
@@ -43,9 +44,15 @@ class PHCMSClient {
43
44
  this.refreshQueue = [];
44
45
  this.normalizedApiPrefix = `/${(config.apiPrefix || '/api').replace(/^\/+|\/+$/g, '')}`;
45
46
  const normalizedApiPrefix = this.normalizedApiPrefix;
47
+ // If tokens are provided directly (SSR use case), and no auth provider is set,
48
+ // wrap them in a StaticAuthProvider to ensure interceptors work.
49
+ if ((config.accessToken || config.refreshToken) && !config.auth) {
50
+ this.config.auth = new static_provider_1.StaticAuthProvider(config.accessToken || null, config.refreshToken || null);
51
+ }
46
52
  this.axiosInstance = config.axiosInstance || axios_1.default.create({
47
53
  baseURL: config.baseURL,
48
54
  timeout: config.timeout || 10000,
55
+ withCredentials: true,
49
56
  headers: {
50
57
  'Content-Type': 'application/json',
51
58
  },
@@ -43,6 +43,16 @@ export declare const useUser: () => {
43
43
  isAuthenticated: boolean;
44
44
  error: boolean;
45
45
  };
46
+ /**
47
+ * Hook to fetch a user's public profile.
48
+ */
49
+ export declare const useUserProfile: (uid: string) => import("@tanstack/react-query").UseQueryResult<{
50
+ uid: string;
51
+ username: string | null;
52
+ display_name: string;
53
+ avatar_url: string | null;
54
+ profile_data: Record<string, any>;
55
+ }, Error>;
46
56
  /**
47
57
  * Hook to update a user's profile.
48
58
  * Typically used by a user to update their own profile.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useAgreeTerms = exports.useChannelTerms = exports.useUpdateProfile = exports.useUser = exports.termsKeys = void 0;
3
+ exports.useAgreeTerms = exports.useChannelTerms = exports.useUpdateProfile = exports.useUserProfile = exports.useUser = exports.termsKeys = void 0;
4
4
  const react_query_1 = require("@tanstack/react-query");
5
5
  const context_1 = require("../context");
6
6
  exports.termsKeys = {
@@ -15,6 +15,18 @@ const useUser = () => {
15
15
  return { data: user, isLoading, isAuthenticated, error: isError };
16
16
  };
17
17
  exports.useUser = useUser;
18
+ /**
19
+ * Hook to fetch a user's public profile.
20
+ */
21
+ const useUserProfile = (uid) => {
22
+ const client = (0, context_1.usePHCMS)();
23
+ return (0, react_query_1.useQuery)({
24
+ queryKey: ['user-profile', uid],
25
+ queryFn: () => client.user.getProfile(uid),
26
+ enabled: !!uid,
27
+ });
28
+ };
29
+ exports.useUserProfile = useUserProfile;
18
30
  /**
19
31
  * Hook to update a user's profile.
20
32
  * Typically used by a user to update their own profile.
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './auth/firebase-provider';
3
3
  export * from './auth/interfaces';
4
4
  export * from './auth/jwt-utils';
5
5
  export * from './auth/local-provider';
6
+ export * from './auth/static-provider';
6
7
  export * from './client';
7
8
  export * from './errors';
8
9
  export * from './modules/auth';
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ __exportStar(require("./auth/firebase-provider"), exports);
19
19
  __exportStar(require("./auth/interfaces"), exports);
20
20
  __exportStar(require("./auth/jwt-utils"), exports);
21
21
  __exportStar(require("./auth/local-provider"), exports);
22
+ __exportStar(require("./auth/static-provider"), exports);
22
23
  __exportStar(require("./client"), exports);
23
24
  __exportStar(require("./errors"), exports);
24
25
  __exportStar(require("./modules/auth"), exports);
@@ -1,9 +1,15 @@
1
- import { UpdateUserProfileRequest, UserDto } from "@ph-cms/api-contract";
1
+ import { UpdateUserProfileRequest, UserDto, UserProfileDto } from "@ph-cms/api-contract";
2
2
  import { AxiosInstance } from "axios";
3
3
  export declare class UserModule {
4
4
  private client;
5
5
  private prefix;
6
6
  constructor(client: AxiosInstance, prefix?: string);
7
+ /**
8
+ * Retrieves a user's public profile.
9
+ *
10
+ * @param uid - The UID of the user.
11
+ */
12
+ getProfile(uid: string): Promise<UserProfileDto>;
7
13
  /**
8
14
  * Updates a user's profile.
9
15
  *
@@ -6,6 +6,14 @@ class UserModule {
6
6
  this.client = client;
7
7
  this.prefix = prefix;
8
8
  }
9
+ /**
10
+ * Retrieves a user's public profile.
11
+ *
12
+ * @param uid - The UID of the user.
13
+ */
14
+ async getProfile(uid) {
15
+ return this.client.get(`${this.prefix}/users/${uid}/profile`);
16
+ }
9
17
  /**
10
18
  * Updates a user's profile.
11
19
  *
package/dist/types.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { AuthStatus, PHCMSContextType, PHCMSProviderProps } from './context
5
5
  export type { FirebaseAuthSyncProps, UseFirebaseAuthSyncOptions, UseFirebaseAuthSyncReturn } from './hooks/useFirebaseAuthSync';
6
6
  export type { StampAvailability, CheckStampAvailabilityParams } from './hooks/useStampTour';
7
7
  export type { AuthResponse, FirebaseExchangeRequest, LoginRequest, RefreshTokenRequest, RegisterRequest } from '@ph-cms/api-contract';
8
- export type { UserDto } from '@ph-cms/api-contract';
8
+ export type { UserDto, UserProfileDto } from '@ph-cms/api-contract';
9
9
  export type { ChannelDto, CheckHierarchyQuery } from '@ph-cms/api-contract';
10
10
  export type { ContentDto, ContentStatDto, ContentMediaDto, CreateContentRequest, ListContentQuery, PagedContentListResponse, UpdateContentRequest } from '@ph-cms/api-contract';
11
11
  export type { HierarchySetDto } from '@ph-cms/api-contract';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ph-cms/client-sdk",
3
- "version": "0.1.27",
3
+ "version": "0.1.30",
4
4
  "description": "Unified PH-CMS Client SDK (React + Core)",
5
5
  "keywords": [],
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "dependencies": {
33
- "@ph-cms/api-contract": "^0.1.10",
33
+ "@ph-cms/api-contract": "^0.1.11",
34
34
  "@tanstack/react-query": "^5.0.0",
35
35
  "axios": "^1.6.0",
36
36
  "zod": "^3.22.4"