@lemondouble/lemon-auth 0.9.1 → 0.11.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/LICENSE +21 -0
- package/README.md +81 -588
- package/dist/{chunk-OX5MDVSR.js → chunk-JIXVTKLG.js} +1 -4
- package/dist/{chunk-OX5MDVSR.js.map → chunk-JIXVTKLG.js.map} +1 -1
- package/dist/chunk-TKMZEMW7.js +73 -0
- package/dist/chunk-TKMZEMW7.js.map +1 -0
- package/dist/client/index.d.ts +3 -36
- package/dist/client/index.js +6 -122
- package/dist/client/index.js.map +1 -1
- package/dist/proxy/index.d.ts +1 -13
- package/dist/proxy/index.js +230 -31
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.d.ts +8 -26
- package/dist/server/index.js +33 -74
- package/dist/server/index.js.map +1 -1
- package/dist/url-O6F1o0px.d.ts +24 -0
- package/package.json +1 -1
- package/dist/types-B5uRY2Si.d.ts +0 -34
|
@@ -8,7 +8,6 @@ var PROFILE_URL = `${AUTH_SERVER_URL}/api/user/me`;
|
|
|
8
8
|
var PROFILE_PAGE_URL = `${AUTH_SERVER_URL}/profile`;
|
|
9
9
|
var ACCESS_TOKEN_COOKIE = "lemon_access_token";
|
|
10
10
|
var REFRESH_TOKEN_COOKIE = "lemon_refresh_token";
|
|
11
|
-
var DEVICE_ID_COOKIE = "device_id";
|
|
12
11
|
|
|
13
12
|
// src/mock.ts
|
|
14
13
|
var LEMON_AUTH_MOCK_USER_ENV = "NEXT_PUBLIC_LEMON_AUTH_MOCK_USER";
|
|
@@ -109,12 +108,10 @@ export {
|
|
|
109
108
|
LOGIN_URL,
|
|
110
109
|
REFRESH_URL,
|
|
111
110
|
LOGOUT_URL,
|
|
112
|
-
PROFILE_URL,
|
|
113
111
|
PROFILE_PAGE_URL,
|
|
114
112
|
ACCESS_TOKEN_COOKIE,
|
|
115
113
|
REFRESH_TOKEN_COOKIE,
|
|
116
|
-
DEVICE_ID_COOKIE,
|
|
117
114
|
isMockAuthEnabled,
|
|
118
115
|
getMockAccessTokenClaims
|
|
119
116
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
117
|
+
//# sourceMappingURL=chunk-JIXVTKLG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/mock.ts"],"sourcesContent":["export const AUTH_SERVER_URL = \"https://auth.lemondouble.com\";\nexport const JWKS_URL = `${AUTH_SERVER_URL}/.well-known/jwks.json`;\nexport const LOGIN_URL = `${AUTH_SERVER_URL}/api/oauth2/google/login`;\nexport const REFRESH_URL = `${AUTH_SERVER_URL}/api/token/refresh`;\nexport const LOGOUT_URL = `${AUTH_SERVER_URL}/api/token/logout`;\nexport const PROFILE_URL = `${AUTH_SERVER_URL}/api/user/me`;\nexport const PROFILE_PAGE_URL = `${AUTH_SERVER_URL}/profile`;\n\nexport const ACCESS_TOKEN_COOKIE = \"lemon_access_token\";\nexport const REFRESH_TOKEN_COOKIE = \"lemon_refresh_token\";\nexport const DEVICE_ID_COOKIE = \"device_id\";\n","import type { AccessTokenClaims, LemonUser } from \"./types.js\";\n\nexport const LEMON_AUTH_MOCK_USER_ENV =\n \"NEXT_PUBLIC_LEMON_AUTH_MOCK_USER\";\n\nexport function isMockAuthEnabled(): boolean {\n return !isProduction() && Boolean(readMockUserEnv());\n}\n\nexport function getMockUser(): LemonUser | null {\n const raw = readMockUserEnv();\n if (isProduction() || !raw) return null;\n return parseMockUser(raw);\n}\n\nexport function getMockAccessTokenClaims(): AccessTokenClaims | null {\n const user = getMockUser();\n if (!user) return null;\n\n const now = Math.floor(Date.now() / 1000);\n return {\n token_type: \"access\",\n sub: user.uid,\n nickname: user.nickname,\n profile_image_url: user.profileImageUrl,\n role: user.role,\n approved_clients: user.approvedClients,\n iss: \"lemon-auth-mock\",\n iat: now,\n exp: now + 60 * 60 * 24 * 365,\n };\n}\n\nfunction readMockUserEnv(): string | undefined {\n try {\n return process.env.NEXT_PUBLIC_LEMON_AUTH_MOCK_USER;\n } catch {\n return undefined;\n }\n}\n\nfunction isProduction(): boolean {\n try {\n return process.env.NODE_ENV === \"production\";\n } catch {\n return false;\n }\n}\n\nfunction parseMockUser(raw: string): LemonUser {\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch {\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV} must be a valid JSON object`\n );\n }\n\n if (!isRecord(data)) {\n throw new Error(`${LEMON_AUTH_MOCK_USER_ENV} must be a JSON object`);\n }\n\n return {\n uid: readOptionalString(data.uid, \"local-user\", \"uid\"),\n nickname: readOptionalString(data.nickname, \"Local User\", \"nickname\"),\n profileImageUrl: readOptionalString(\n data.profileImageUrl ?? data.profile_image_url,\n \"\",\n \"profileImageUrl\"\n ),\n role: readOptionalRole(data.role, \"admin\"),\n approvedClients: readOptionalStringArray(\n data.approvedClients ?? data.approved_clients,\n [\"*\"],\n \"approvedClients\"\n ),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readOptionalString(\n value: unknown,\n fallback: string,\n field: string\n): string {\n if (value === undefined || value === null) return fallback;\n if (typeof value === \"string\") return value;\n throw new Error(`${LEMON_AUTH_MOCK_USER_ENV}.${field} must be a string`);\n}\n\nfunction readOptionalRole(\n value: unknown,\n fallback: LemonUser[\"role\"]\n): LemonUser[\"role\"] {\n if (value === undefined || value === null) return fallback;\n if (value === \"user\" || value === \"admin\") return value;\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV}.role must be \"user\" or \"admin\"`\n );\n}\n\nfunction readOptionalStringArray(\n value: unknown,\n fallback: string[],\n field: string\n): string[] {\n if (value === undefined || value === null) return fallback;\n if (\n Array.isArray(value) &&\n value.every((item) => typeof item === \"string\")\n ) {\n return value;\n }\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV}.${field} must be a string array`\n );\n}\n"],"mappings":";AAAO,IAAM,kBAAkB;AACxB,IAAM,WAAW,GAAG,eAAe;AACnC,IAAM,YAAY,GAAG,eAAe;AACpC,IAAM,cAAc,GAAG,eAAe;AACtC,IAAM,aAAa,GAAG,eAAe;AACrC,IAAM,cAAc,GAAG,eAAe;AACtC,IAAM,mBAAmB,GAAG,eAAe;AAE3C,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/mock.ts"],"sourcesContent":["export const AUTH_SERVER_URL = \"https://auth.lemondouble.com\";\nexport const JWKS_URL = `${AUTH_SERVER_URL}/.well-known/jwks.json`;\nexport const LOGIN_URL = `${AUTH_SERVER_URL}/api/oauth2/google/login`;\nexport const REFRESH_URL = `${AUTH_SERVER_URL}/api/token/refresh`;\nexport const LOGOUT_URL = `${AUTH_SERVER_URL}/api/token/logout`;\nexport const PROFILE_URL = `${AUTH_SERVER_URL}/api/user/me`;\nexport const PROFILE_PAGE_URL = `${AUTH_SERVER_URL}/profile`;\n\nexport const ACCESS_TOKEN_COOKIE = \"lemon_access_token\";\nexport const REFRESH_TOKEN_COOKIE = \"lemon_refresh_token\";\nexport const DEVICE_ID_COOKIE = \"device_id\";\n","import type { AccessTokenClaims, LemonUser } from \"./types.js\";\n\nexport const LEMON_AUTH_MOCK_USER_ENV =\n \"NEXT_PUBLIC_LEMON_AUTH_MOCK_USER\";\n\nexport function isMockAuthEnabled(): boolean {\n return !isProduction() && Boolean(readMockUserEnv());\n}\n\nexport function getMockUser(): LemonUser | null {\n const raw = readMockUserEnv();\n if (isProduction() || !raw) return null;\n return parseMockUser(raw);\n}\n\nexport function getMockAccessTokenClaims(): AccessTokenClaims | null {\n const user = getMockUser();\n if (!user) return null;\n\n const now = Math.floor(Date.now() / 1000);\n return {\n token_type: \"access\",\n sub: user.uid,\n nickname: user.nickname,\n profile_image_url: user.profileImageUrl,\n role: user.role,\n approved_clients: user.approvedClients,\n iss: \"lemon-auth-mock\",\n iat: now,\n exp: now + 60 * 60 * 24 * 365,\n };\n}\n\nfunction readMockUserEnv(): string | undefined {\n try {\n return process.env.NEXT_PUBLIC_LEMON_AUTH_MOCK_USER;\n } catch {\n return undefined;\n }\n}\n\nfunction isProduction(): boolean {\n try {\n return process.env.NODE_ENV === \"production\";\n } catch {\n return false;\n }\n}\n\nfunction parseMockUser(raw: string): LemonUser {\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch {\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV} must be a valid JSON object`\n );\n }\n\n if (!isRecord(data)) {\n throw new Error(`${LEMON_AUTH_MOCK_USER_ENV} must be a JSON object`);\n }\n\n return {\n uid: readOptionalString(data.uid, \"local-user\", \"uid\"),\n nickname: readOptionalString(data.nickname, \"Local User\", \"nickname\"),\n profileImageUrl: readOptionalString(\n data.profileImageUrl ?? data.profile_image_url,\n \"\",\n \"profileImageUrl\"\n ),\n role: readOptionalRole(data.role, \"admin\"),\n approvedClients: readOptionalStringArray(\n data.approvedClients ?? data.approved_clients,\n [\"*\"],\n \"approvedClients\"\n ),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readOptionalString(\n value: unknown,\n fallback: string,\n field: string\n): string {\n if (value === undefined || value === null) return fallback;\n if (typeof value === \"string\") return value;\n throw new Error(`${LEMON_AUTH_MOCK_USER_ENV}.${field} must be a string`);\n}\n\nfunction readOptionalRole(\n value: unknown,\n fallback: LemonUser[\"role\"]\n): LemonUser[\"role\"] {\n if (value === undefined || value === null) return fallback;\n if (value === \"user\" || value === \"admin\") return value;\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV}.role must be \"user\" or \"admin\"`\n );\n}\n\nfunction readOptionalStringArray(\n value: unknown,\n fallback: string[],\n field: string\n): string[] {\n if (value === undefined || value === null) return fallback;\n if (\n Array.isArray(value) &&\n value.every((item) => typeof item === \"string\")\n ) {\n return value;\n }\n throw new Error(\n `${LEMON_AUTH_MOCK_USER_ENV}.${field} must be a string array`\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOGIN_URL,
|
|
3
|
+
LOGOUT_URL,
|
|
4
|
+
PROFILE_PAGE_URL,
|
|
5
|
+
REFRESH_URL,
|
|
6
|
+
isMockAuthEnabled
|
|
7
|
+
} from "./chunk-JIXVTKLG.js";
|
|
8
|
+
|
|
9
|
+
// src/client/url.ts
|
|
10
|
+
function loginUrl(redirectUrl) {
|
|
11
|
+
if (isMockAuthEnabled()) return redirectUrl;
|
|
12
|
+
assertValidLoginRedirectUrl(redirectUrl);
|
|
13
|
+
return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
|
|
14
|
+
}
|
|
15
|
+
function profileUrl(redirectUrl) {
|
|
16
|
+
if (isMockAuthEnabled()) return redirectUrl ?? "/";
|
|
17
|
+
if (redirectUrl) {
|
|
18
|
+
return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
|
|
19
|
+
}
|
|
20
|
+
return PROFILE_PAGE_URL;
|
|
21
|
+
}
|
|
22
|
+
async function logout() {
|
|
23
|
+
if (isMockAuthEnabled()) return true;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(LOGOUT_URL, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
credentials: "include"
|
|
28
|
+
});
|
|
29
|
+
return res.ok;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function refreshToken() {
|
|
35
|
+
if (isMockAuthEnabled()) return true;
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(REFRESH_URL, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
credentials: "include"
|
|
40
|
+
});
|
|
41
|
+
return res.ok;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertValidLoginRedirectUrl(redirectUrl) {
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(redirectUrl);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("loginUrl redirectUrl must be a valid absolute URL");
|
|
52
|
+
}
|
|
53
|
+
if (url.protocol !== "https:") {
|
|
54
|
+
throw new Error("loginUrl redirectUrl must use https");
|
|
55
|
+
}
|
|
56
|
+
if (!isAllowedLoginRedirectHost(url.hostname)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"loginUrl redirectUrl host must be lemondouble.com or a subdomain of lemondouble.com"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isAllowedLoginRedirectHost(hostname) {
|
|
63
|
+
const normalized = hostname.toLowerCase();
|
|
64
|
+
return normalized === "lemondouble.com" || normalized.endsWith(".lemondouble.com");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
loginUrl,
|
|
69
|
+
profileUrl,
|
|
70
|
+
logout,
|
|
71
|
+
refreshToken
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=chunk-TKMZEMW7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/url.ts"],"sourcesContent":["import {\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\n\nexport function loginUrl(redirectUrl: string): string {\n if (isMockAuthEnabled()) return redirectUrl;\n assertValidLoginRedirectUrl(redirectUrl);\n return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n}\n\nexport function profileUrl(redirectUrl?: string): string {\n if (isMockAuthEnabled()) return redirectUrl ?? \"/\";\n if (redirectUrl) {\n return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n }\n return PROFILE_PAGE_URL;\n}\n\n/** 쿠키는 httpOnly 라 삭제는 응답의 Set-Cookie 가 한다. */\nexport async function logout(): Promise<boolean> {\n if (isMockAuthEnabled()) return true;\n\n try {\n const res = await fetch(LOGOUT_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\nexport async function refreshToken(): Promise<boolean> {\n if (isMockAuthEnabled()) return true;\n\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\nfunction assertValidLoginRedirectUrl(redirectUrl: string): void {\n let url: URL;\n try {\n url = new URL(redirectUrl);\n } catch {\n throw new Error(\"loginUrl redirectUrl must be a valid absolute URL\");\n }\n\n if (url.protocol !== \"https:\") {\n throw new Error(\"loginUrl redirectUrl must use https\");\n }\n\n if (!isAllowedLoginRedirectHost(url.hostname)) {\n throw new Error(\n \"loginUrl redirectUrl host must be lemondouble.com or a subdomain of lemondouble.com\"\n );\n }\n}\n\nfunction isAllowedLoginRedirectHost(hostname: string): boolean {\n const normalized = hostname.toLowerCase();\n return (\n normalized === \"lemondouble.com\" ||\n normalized.endsWith(\".lemondouble.com\")\n );\n}\n"],"mappings":";;;;;;;;;AAQO,SAAS,SAAS,aAA6B;AACpD,MAAI,kBAAkB,EAAG,QAAO;AAChC,8BAA4B,WAAW;AACvC,SAAO,GAAG,SAAS,iBAAiB,mBAAmB,WAAW,CAAC;AACrE;AAEO,SAAS,WAAW,aAA8B;AACvD,MAAI,kBAAkB,EAAG,QAAO,eAAe;AAC/C,MAAI,aAAa;AACf,WAAO,GAAG,gBAAgB,iBAAiB,mBAAmB,WAAW,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAGA,eAAsB,SAA2B;AAC/C,MAAI,kBAAkB,EAAG,QAAO;AAEhC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,YAAY;AAAA,MAClC,QAAQ;AAAA,MACR,aAAa;AAAA,IACf,CAAC;AACD,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,eAAiC;AACrD,MAAI,kBAAkB,EAAG,QAAO;AAEhC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,aAAa;AAAA,MACnC,QAAQ;AAAA,MACR,aAAa;AAAA,IACf,CAAC;AACD,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,4BAA4B,aAA2B;AAC9D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,WAAW;AAAA,EAC3B,QAAQ;AACN,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,MAAI,IAAI,aAAa,UAAU;AAC7B,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AAEA,MAAI,CAAC,2BAA2B,IAAI,QAAQ,GAAG;AAC7C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,2BAA2B,UAA2B;AAC7D,QAAM,aAAa,SAAS,YAAY;AACxC,SACE,eAAe,qBACf,WAAW,SAAS,kBAAkB;AAE1C;","names":[]}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { a as LemonUser } from '../
|
|
3
|
-
|
|
4
|
-
import 'jose';
|
|
2
|
+
import { a as LemonUser } from '../url-O6F1o0px.js';
|
|
3
|
+
export { l as loginUrl, b as logout, p as profileUrl, r as refreshToken } from '../url-O6F1o0px.js';
|
|
5
4
|
|
|
6
5
|
interface AuthContextValue {
|
|
7
6
|
user: LemonUser | null;
|
|
@@ -13,36 +12,4 @@ declare function AuthProvider({ user, children, }: {
|
|
|
13
12
|
}): react_jsx_runtime.JSX.Element;
|
|
14
13
|
declare function useAuth(): AuthContextValue;
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
* 토큰을 갱신하는 유일한 주체. 프록시가 만료된 세션을 이리로 보낸다.
|
|
18
|
-
*
|
|
19
|
-
* 갱신을 서버(미들웨어)가 아니라 브라우저가 하는 이유는 두 가지다.
|
|
20
|
-
*
|
|
21
|
-
* 1. 미들웨어는 요청마다 별도 인스턴스로 떠서 서로를 모른다. 프리페치가
|
|
22
|
-
* 깔리면 같은 refresh token 으로 갱신이 동시에 나가고, 회전 경쟁이 난다.
|
|
23
|
-
* 브라우저의 복구 경로는 한 번에 하나만 뜬다.
|
|
24
|
-
* 2. 브라우저 요청에는 진짜 UA 와 WAF 통과 쿠키가 실린다. 서버간 fetch 는
|
|
25
|
-
* UA 가 런타임 기본값이라 봇 방어에 걸릴 수 있다.
|
|
26
|
-
*
|
|
27
|
-
* 끝나면 router 가 아니라 window.location 으로 next 에 간다. 소프트 네비는
|
|
28
|
-
* 루트 레이아웃(과 AuthProvider)을 다시 렌더하지 않으므로, 옛 세션 스냅샷이
|
|
29
|
-
* 남아 화면이 어긋나거나 라우터 캐시에 굳은 리다이렉트가 되살아난다.
|
|
30
|
-
*
|
|
31
|
-
* 실패해도 next 로 간다. 죽은 토큰이면 서버가 401 응답에서 세션 쿠키를
|
|
32
|
-
* 지워 주므로(auth-server v2026.08.03.2+), 이후 판단 — 로그인으로 보낼지,
|
|
33
|
-
* 익명으로 보여줄지 — 는 프록시와 DAL 이 알아서 한다.
|
|
34
|
-
*/
|
|
35
|
-
declare function SessionRestore({ fallback }: {
|
|
36
|
-
fallback: React.ReactNode;
|
|
37
|
-
}): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
38
|
-
|
|
39
|
-
declare function loginUrl(redirectUrl: string): string;
|
|
40
|
-
declare function profileUrl(redirectUrl?: string): string;
|
|
41
|
-
/**
|
|
42
|
-
* 로그아웃. 쿠키 삭제는 응답의 Set-Cookie 가 한다 — 세션 쿠키는 httpOnly
|
|
43
|
-
* 라서 JS 로는 지울 수 없다.
|
|
44
|
-
*/
|
|
45
|
-
declare function logout(): Promise<boolean>;
|
|
46
|
-
declare function refreshToken(): Promise<boolean>;
|
|
47
|
-
|
|
48
|
-
export { AuthProvider, LemonUser, SessionRestore, loginUrl, logout, profileUrl, refreshToken, useAuth };
|
|
15
|
+
export { AuthProvider, LemonUser, useAuth };
|
package/dist/client/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
loginUrl,
|
|
4
|
+
logout,
|
|
5
|
+
profileUrl,
|
|
6
|
+
refreshToken
|
|
7
|
+
} from "../chunk-TKMZEMW7.js";
|
|
8
|
+
import "../chunk-JIXVTKLG.js";
|
|
9
9
|
|
|
10
10
|
// src/client/context.tsx
|
|
11
11
|
import { createContext, useContext } from "react";
|
|
@@ -23,124 +23,8 @@ function AuthProvider({
|
|
|
23
23
|
function useAuth() {
|
|
24
24
|
return useContext(AuthContext);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
// src/client/session-restore.tsx
|
|
28
|
-
import { useEffect, useState } from "react";
|
|
29
|
-
import { useSearchParams } from "next/navigation";
|
|
30
|
-
|
|
31
|
-
// src/client/url.ts
|
|
32
|
-
function loginUrl(redirectUrl) {
|
|
33
|
-
if (isMockAuthEnabled()) return redirectUrl;
|
|
34
|
-
assertValidLoginRedirectUrl(redirectUrl);
|
|
35
|
-
return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
|
|
36
|
-
}
|
|
37
|
-
function profileUrl(redirectUrl) {
|
|
38
|
-
if (isMockAuthEnabled()) return redirectUrl ?? "/";
|
|
39
|
-
if (redirectUrl) {
|
|
40
|
-
return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
|
|
41
|
-
}
|
|
42
|
-
return PROFILE_PAGE_URL;
|
|
43
|
-
}
|
|
44
|
-
async function logout() {
|
|
45
|
-
if (isMockAuthEnabled()) return true;
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(LOGOUT_URL, {
|
|
48
|
-
method: "POST",
|
|
49
|
-
credentials: "include"
|
|
50
|
-
});
|
|
51
|
-
return res.ok;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function refreshSession() {
|
|
57
|
-
if (isMockAuthEnabled()) return "ok";
|
|
58
|
-
try {
|
|
59
|
-
const res = await fetch(REFRESH_URL, {
|
|
60
|
-
method: "POST",
|
|
61
|
-
credentials: "include"
|
|
62
|
-
});
|
|
63
|
-
if (res.ok) return "ok";
|
|
64
|
-
if (res.status === 401 || res.status === 403) return "unauthorized";
|
|
65
|
-
return "unavailable";
|
|
66
|
-
} catch {
|
|
67
|
-
return "unavailable";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async function refreshToken() {
|
|
71
|
-
return await refreshSession() === "ok";
|
|
72
|
-
}
|
|
73
|
-
function assertValidLoginRedirectUrl(redirectUrl) {
|
|
74
|
-
let url;
|
|
75
|
-
try {
|
|
76
|
-
url = new URL(redirectUrl);
|
|
77
|
-
} catch {
|
|
78
|
-
throw new Error("loginUrl redirectUrl must be a valid absolute URL");
|
|
79
|
-
}
|
|
80
|
-
if (url.protocol !== "https:") {
|
|
81
|
-
throw new Error("loginUrl redirectUrl must use https");
|
|
82
|
-
}
|
|
83
|
-
if (!isAllowedLoginRedirectHost(url.hostname)) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"loginUrl redirectUrl host must be lemondouble.com or a subdomain of lemondouble.com"
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function isAllowedLoginRedirectHost(hostname) {
|
|
90
|
-
const normalized = hostname.toLowerCase();
|
|
91
|
-
return normalized === "lemondouble.com" || normalized.endsWith(".lemondouble.com");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/client/session-restore.tsx
|
|
95
|
-
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
96
|
-
function SessionRestore({ fallback }) {
|
|
97
|
-
const searchParams = useSearchParams();
|
|
98
|
-
const [unavailable, setUnavailable] = useState(false);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
let ignore = false;
|
|
101
|
-
const next = safeNextPath(searchParams.get("next"));
|
|
102
|
-
refreshSession().then((result) => {
|
|
103
|
-
if (ignore) return;
|
|
104
|
-
if (result === "unavailable") {
|
|
105
|
-
setUnavailable(true);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
window.location.replace(next);
|
|
109
|
-
});
|
|
110
|
-
return () => {
|
|
111
|
-
ignore = true;
|
|
112
|
-
};
|
|
113
|
-
}, [searchParams]);
|
|
114
|
-
if (unavailable) {
|
|
115
|
-
return /* @__PURE__ */ jsxs(
|
|
116
|
-
"div",
|
|
117
|
-
{
|
|
118
|
-
style: {
|
|
119
|
-
minHeight: "100dvh",
|
|
120
|
-
display: "flex",
|
|
121
|
-
flexDirection: "column",
|
|
122
|
-
alignItems: "center",
|
|
123
|
-
justifyContent: "center",
|
|
124
|
-
gap: 12
|
|
125
|
-
},
|
|
126
|
-
children: [
|
|
127
|
-
/* @__PURE__ */ jsx2("p", { children: "\uC778\uC99D \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." }),
|
|
128
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => window.location.reload(), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })
|
|
129
|
-
]
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return fallback;
|
|
134
|
-
}
|
|
135
|
-
function safeNextPath(next) {
|
|
136
|
-
if (!next) return "/";
|
|
137
|
-
if (!next.startsWith("/")) return "/";
|
|
138
|
-
if (next.startsWith("//")) return "/";
|
|
139
|
-
return next;
|
|
140
|
-
}
|
|
141
26
|
export {
|
|
142
27
|
AuthProvider,
|
|
143
|
-
SessionRestore,
|
|
144
28
|
loginUrl,
|
|
145
29
|
logout,
|
|
146
30
|
profileUrl,
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/context.tsx"
|
|
1
|
+
{"version":3,"sources":["../../src/client/context.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { LemonUser } from \"../types.js\";\n\ninterface AuthContextValue {\n user: LemonUser | null;\n isAuthenticated: boolean;\n}\n\nconst AuthContext = createContext<AuthContextValue>({\n user: null,\n isAuthenticated: false,\n});\n\nexport function AuthProvider({\n user,\n children,\n}: {\n user: LemonUser | null;\n children: React.ReactNode;\n}) {\n return (\n <AuthContext value={{ user, isAuthenticated: user !== null }}>\n {children}\n </AuthContext>\n );\n}\n\nexport function useAuth(): AuthContextValue {\n return useContext(AuthContext);\n}\n"],"mappings":";;;;;;;;;AAEA,SAAS,eAAe,kBAAkB;AAqBtC;AAbJ,IAAM,cAAc,cAAgC;AAAA,EAClD,MAAM;AAAA,EACN,iBAAiB;AACnB,CAAC;AAEM,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,SACE,oBAAC,eAAY,OAAO,EAAE,MAAM,iBAAiB,SAAS,KAAK,GACxD,UACH;AAEJ;AAEO,SAAS,UAA4B;AAC1C,SAAO,WAAW,WAAW;AAC/B;","names":[]}
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
|
|
3
|
-
declare const DEFAULT_AUTH_BYPASS_PATHS: string[];
|
|
4
3
|
declare const DEFAULT_API_PATHS: string[];
|
|
5
|
-
/** 앱이 <SessionRestore /> 를 마운트해야 하는 기본 경로. */
|
|
6
|
-
declare const DEFAULT_RESTORE_PATH = "/auth/restore";
|
|
7
|
-
declare const PROXY_AUTH_ERROR: {
|
|
8
|
-
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
9
|
-
readonly FORBIDDEN: "FORBIDDEN";
|
|
10
|
-
};
|
|
11
|
-
type ProxyAuthErrorCode = (typeof PROXY_AUTH_ERROR)[keyof typeof PROXY_AUTH_ERROR];
|
|
12
4
|
type LoginRedirectUrl = string | ((request: NextRequest) => string);
|
|
13
5
|
interface AuthProxyOptions {
|
|
14
6
|
/** 로그인 없이 열어둘 경로. 익명이면 통과, 만료 세션이면 복구를 거친다. */
|
|
15
7
|
publicPaths?: string[];
|
|
16
|
-
/** 프록시 자체를 건너뛸 경로. 정적 파일 등. */
|
|
17
|
-
bypassPaths?: string[];
|
|
18
8
|
/** 리다이렉트 대신 401 JSON 을 돌려줄 경로. 기본 `/api/*`. */
|
|
19
9
|
apiPaths?: string[];
|
|
20
10
|
loginRedirectUrl?: LoginRedirectUrl;
|
|
21
|
-
/** <SessionRestore /> 를 마운트한 경로. 기본 `/auth/restore`. */
|
|
22
|
-
restorePath?: string;
|
|
23
11
|
}
|
|
24
12
|
declare function createAuthProxy(options?: AuthProxyOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
25
13
|
|
|
26
|
-
export { type AuthProxyOptions, DEFAULT_API_PATHS,
|
|
14
|
+
export { type AuthProxyOptions, DEFAULT_API_PATHS, type LoginRedirectUrl, createAuthProxy };
|