@ecomcx/nexus-theme-sdk 0.0.0-stage → 1.24.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/dist/api.d.ts +92 -0
- package/dist/api.js +149 -0
- package/dist/auth-forms.d.ts +83 -0
- package/dist/auth-forms.js +206 -0
- package/dist/auth-pages.d.ts +6 -0
- package/dist/auth-pages.js +33 -0
- package/dist/auth-shared.d.ts +14 -0
- package/dist/auth-shared.js +7 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +48 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +191 -0
- package/dist/components.d.ts +68 -0
- package/dist/components.js +154 -0
- package/dist/content.d.ts +25 -0
- package/dist/content.js +59 -0
- package/dist/dynamic.d.ts +9 -0
- package/dist/dynamic.js +128 -0
- package/dist/i18n.d.ts +11 -0
- package/dist/i18n.js +104 -0
- package/dist/image.d.ts +24 -0
- package/dist/image.js +37 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +21 -0
- package/dist/locale.d.ts +9 -0
- package/dist/locale.js +20 -0
- package/dist/metadata.d.ts +3 -0
- package/dist/metadata.js +26 -0
- package/dist/page-canvas.d.ts +4 -0
- package/dist/page-canvas.js +7 -0
- package/dist/page-full-width.d.ts +3 -0
- package/dist/page-full-width.js +6 -0
- package/dist/plugin-form-client.d.ts +27 -0
- package/dist/plugin-form-client.js +55 -0
- package/dist/plugin-form.d.ts +15 -0
- package/dist/plugin-form.js +25 -0
- package/dist/preferences.d.ts +9 -0
- package/dist/preferences.js +17 -0
- package/dist/rich.d.ts +66 -0
- package/dist/rich.js +388 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.js +238 -0
- package/dist/social-icons.d.ts +5 -0
- package/dist/social-icons.js +21 -0
- package/dist/template-blocks.d.ts +3 -0
- package/dist/template-blocks.js +11 -0
- package/dist/types.d.ts +475 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +29 -0
- package/package.json +62 -4
- package/README.md +0 -3
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PostContentDoc, CommentPage, Menu, Post, ResolveResult, Site, Term, WidgetInstance } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Client gọi Public API của CMS (dùng next-fetch-kit: chạy được cả SSR lẫn CSR,
|
|
4
|
+
* tự gộp request trùng trong cùng một lần render, timeout + retry khi lỗi mạng).
|
|
5
|
+
*/
|
|
6
|
+
export declare const cmsApi: import("next-fetch-kit").FetchKitInstance;
|
|
7
|
+
/**
|
|
8
|
+
* Gọi endpoint public của plugin khi render trên server: pluginFetch("ecomcx-seo", "/sitemap/news").
|
|
9
|
+
* Cache theo tag "cms" (+ tags) như dữ liệu khác của CMS; noStore để luôn lấy mới.
|
|
10
|
+
* asUser: gửi kèm phiên của người đang xem (endpoint nhận ICurrentUser) — luôn không cache: pluginFetch("shop", "/cart", { asUser: true }).
|
|
11
|
+
*/
|
|
12
|
+
export declare function pluginFetch<T>(slug: string, path: string, opts?: FetchOptions): Promise<T>;
|
|
13
|
+
export declare class CmsApiError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
constructor(status: number, message: string);
|
|
16
|
+
}
|
|
17
|
+
export interface FetchOptions {
|
|
18
|
+
tags?: string[];
|
|
19
|
+
noStore?: boolean;
|
|
20
|
+
revalidate?: number;
|
|
21
|
+
query?: Record<string, unknown>;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
asUser?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Gọi Public API của CMS. Kết quả được cache và gắn tag "cms" để revalidate khi nội dung thay đổi. */
|
|
26
|
+
export declare function cmsFetch<T>(path: string, opts?: FetchOptions): Promise<T>;
|
|
27
|
+
/** Thông tin site theo ngôn ngữ của request (hoặc locale truyền vào). */
|
|
28
|
+
export declare const getSite: (locale?: string) => Promise<Site>;
|
|
29
|
+
/** Cookie (httpOnly) giữ token mở khoá bài có mật khẩu: "12:token,34:token". */
|
|
30
|
+
export declare const UNLOCK_COOKIE = "cms-unlock";
|
|
31
|
+
export declare const UNLOCK_HEADER = "X-CMS-Unlock";
|
|
32
|
+
/** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
|
|
33
|
+
export declare const resolvePath: (path: string, query?: Record<string, string | string[] | undefined>, noStore?: boolean, unlock?: string) => Promise<ResolveResult>;
|
|
34
|
+
export declare const getPreview: (token: string) => Promise<ResolveResult>;
|
|
35
|
+
export interface PostQuery {
|
|
36
|
+
/** Mặc định: ngôn ngữ của request hiện tại. */
|
|
37
|
+
locale?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
taxonomy?: string;
|
|
40
|
+
term?: string;
|
|
41
|
+
authorId?: number;
|
|
42
|
+
search?: string;
|
|
43
|
+
include?: number[];
|
|
44
|
+
exclude?: number[];
|
|
45
|
+
parentId?: number;
|
|
46
|
+
/** true = chỉ bài ghim, false = bỏ bài ghim. */
|
|
47
|
+
sticky?: boolean;
|
|
48
|
+
/** Kèm nội dung đầy đủ (post.content). Mặc định không — danh sách chỉ cần tiêu đề, sapo, ảnh, giúp trang nhẹ và nhanh. */
|
|
49
|
+
withContent?: boolean;
|
|
50
|
+
page?: number;
|
|
51
|
+
perPage?: number;
|
|
52
|
+
orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count";
|
|
53
|
+
order?: "asc" | "desc";
|
|
54
|
+
}
|
|
55
|
+
export declare const getPosts: ({ withContent, ...q }?: PostQuery) => Promise<{
|
|
56
|
+
items: Post[];
|
|
57
|
+
total: number;
|
|
58
|
+
page: number;
|
|
59
|
+
totalPages: number;
|
|
60
|
+
}>;
|
|
61
|
+
export declare const getPost: (type: string, slug: string, locale?: string) => Promise<Post | null>;
|
|
62
|
+
export declare const getTerms: (taxonomy: string, hideEmpty?: boolean, locale?: string) => Promise<Term[]>;
|
|
63
|
+
/** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
|
|
64
|
+
export declare const TEMPLATE_DRAFT_COOKIE = "cms-template-draft";
|
|
65
|
+
/** Template / phần dựng bằng khối của theme đang dùng (bản sửa trong Trình dựng thắng bản của theme; đang xem trước → bản nháp). */
|
|
66
|
+
export declare const getTemplates: () => Promise<{
|
|
67
|
+
theme: string;
|
|
68
|
+
templates: Record<string, PostContentDoc>;
|
|
69
|
+
parts: Record<string, PostContentDoc>;
|
|
70
|
+
}>;
|
|
71
|
+
/** Nội dung mẫu đồng bộ (khối syncedPattern). null = mẫu đã xoá / chưa xuất bản. */
|
|
72
|
+
export declare const getPattern: (id: number) => Promise<PostContentDoc | null>;
|
|
73
|
+
export declare const getMenu: (location: string, locale?: string) => Promise<Menu | null>;
|
|
74
|
+
export declare const getMenuById: (id: number) => Promise<Menu | null>;
|
|
75
|
+
export declare const getWidgets: (area: string, locale?: string) => Promise<WidgetInstance[]>;
|
|
76
|
+
/** Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận). */
|
|
77
|
+
export declare const getComments: (postId: number, page?: number) => Promise<CommentPage>;
|
|
78
|
+
export declare const getArchives: (locale?: string) => Promise<{
|
|
79
|
+
year: number;
|
|
80
|
+
month: number;
|
|
81
|
+
count: number;
|
|
82
|
+
link: string;
|
|
83
|
+
}[]>;
|
|
84
|
+
export declare const getLanguages: () => Promise<{
|
|
85
|
+
defaultLocale: string;
|
|
86
|
+
languages: Array<{
|
|
87
|
+
code: string;
|
|
88
|
+
name: string;
|
|
89
|
+
isDefault: boolean;
|
|
90
|
+
flag?: string;
|
|
91
|
+
}>;
|
|
92
|
+
}>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createFetchKit, isFetchKitError } from "next-fetch-kit";
|
|
2
|
+
import { getRequestLocale } from "./locale.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client gọi Public API của CMS (dùng next-fetch-kit: chạy được cả SSR lẫn CSR,
|
|
5
|
+
* tự gộp request trùng trong cùng một lần render, timeout + retry khi lỗi mạng).
|
|
6
|
+
*/
|
|
7
|
+
export const cmsApi = createFetchKit({
|
|
8
|
+
baseURL: `${(process.env.CMS_API_URL ?? "http://localhost:5100").replace(/\/$/, "")}/api/public`,
|
|
9
|
+
timeout: 8_000,
|
|
10
|
+
retry: 1,
|
|
11
|
+
dedupe: true,
|
|
12
|
+
// Khoá bí mật dùng chung với CMS API: request SSR của website không bị rate limit như khách lạ.
|
|
13
|
+
headers: process.env.CMS_FRONTEND_KEY ? { "X-CMS-Frontend-Key": process.env.CMS_FRONTEND_KEY } : undefined,
|
|
14
|
+
});
|
|
15
|
+
/** Client gọi endpoint của plugin (/api/plugins/{slug}/…) từ server — dùng qua pluginFetch(). */
|
|
16
|
+
const pluginApi = createFetchKit({
|
|
17
|
+
baseURL: `${(process.env.CMS_API_URL ?? "http://localhost:5100").replace(/\/$/, "")}/api/plugins`,
|
|
18
|
+
timeout: 8_000,
|
|
19
|
+
retry: 1,
|
|
20
|
+
dedupe: true,
|
|
21
|
+
headers: process.env.CMS_FRONTEND_KEY ? { "X-CMS-Frontend-Key": process.env.CMS_FRONTEND_KEY } : undefined,
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Gọi endpoint public của plugin khi render trên server: pluginFetch("ecomcx-seo", "/sitemap/news").
|
|
25
|
+
* Cache theo tag "cms" (+ tags) như dữ liệu khác của CMS; noStore để luôn lấy mới.
|
|
26
|
+
* asUser: gửi kèm phiên của người đang xem (endpoint nhận ICurrentUser) — luôn không cache: pluginFetch("shop", "/cart", { asUser: true }).
|
|
27
|
+
*/
|
|
28
|
+
export async function pluginFetch(slug, path, opts = {}) {
|
|
29
|
+
if (!/^[a-z0-9][a-z0-9-]{1,63}$/.test(slug) || !path.startsWith("/") || path.includes(".."))
|
|
30
|
+
throw new CmsApiError(400, "Đường dẫn plugin không hợp lệ");
|
|
31
|
+
if (opts.asUser) {
|
|
32
|
+
const { getAccessToken } = await import("./auth.js");
|
|
33
|
+
const token = await getAccessToken();
|
|
34
|
+
opts = { ...opts, noStore: true, headers: { ...opts.headers, ...(token ? { Authorization: `Bearer ${token}` } : {}) } };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const { data } = await pluginApi.get(`/${slug}${path}`, opts.noStore
|
|
38
|
+
? { cache: "no-store", query: opts.query, headers: opts.headers }
|
|
39
|
+
: { next: { tags: ["cms", ...(opts.tags ?? [])], revalidate: opts.revalidate ?? 3600 }, query: opts.query, headers: opts.headers });
|
|
40
|
+
return (data ?? null);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (isFetchKitError(e))
|
|
44
|
+
throw new CmsApiError(e.status ?? 0, `Plugin API ${e.status ?? e.type}: ${slug}${path}`);
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class CmsApiError extends Error {
|
|
49
|
+
status;
|
|
50
|
+
constructor(status, message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.status = status;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* API không phản hồi (thường là đang khởi động lại vài giây khi bật/tắt plugin backend):
|
|
57
|
+
* mọi request đang chờ cùng đợi một lần dò /api/health (tối đa 20 giây — máy chủ yếu khởi động lại chậm hơn) rồi gọi lại, thay vì trả trang lỗi 500.
|
|
58
|
+
* API vẫn chưa lên sau 20 giây → ngắt mạch 5 giây: các lần gọi sau báo lỗi ngay, trang không bị treo hàng phút.
|
|
59
|
+
*/
|
|
60
|
+
const breaker = globalThis;
|
|
61
|
+
const BREAK_MS = 5_000;
|
|
62
|
+
const WAIT_MS = 20_000;
|
|
63
|
+
const API_ROOT = (process.env.CMS_API_URL ?? "http://localhost:5100").replace(/\/$/, "");
|
|
64
|
+
function waitForApi() {
|
|
65
|
+
breaker.__cmsApiRecovering ??= (async () => {
|
|
66
|
+
const until = Date.now() + WAIT_MS;
|
|
67
|
+
while (Date.now() < until) {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${API_ROOT}/api/health`, { cache: "no-store", signal: AbortSignal.timeout(2_000) });
|
|
70
|
+
// "restarting": API sắp tắt để khởi động lại → chờ bản mới
|
|
71
|
+
if (res.ok && (await res.json().catch(() => null))?.status !== "restarting")
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch { /* API chưa lên */ }
|
|
75
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
76
|
+
}
|
|
77
|
+
breaker.__cmsApiDownUntil = Date.now() + BREAK_MS;
|
|
78
|
+
return false;
|
|
79
|
+
})().finally(() => { breaker.__cmsApiRecovering = undefined; });
|
|
80
|
+
return breaker.__cmsApiRecovering;
|
|
81
|
+
}
|
|
82
|
+
/** Lỗi mạng hoặc 502/503/504 (proxy/API đang khởi động) — không tính lỗi 500 thật của API. */
|
|
83
|
+
const isUnavailable = (e) => isFetchKitError(e) && (!e.status || e.status === 502 || e.status === 503 || e.status === 504);
|
|
84
|
+
/** Gọi Public API của CMS. Kết quả được cache và gắn tag "cms" để revalidate khi nội dung thay đổi. */
|
|
85
|
+
export async function cmsFetch(path, opts = {}) {
|
|
86
|
+
if ((breaker.__cmsApiDownUntil ?? 0) > Date.now())
|
|
87
|
+
throw new CmsApiError(503, `CMS API tạm thời không phản hồi: ${path}`);
|
|
88
|
+
const get = () => cmsApi.get(path, opts.noStore
|
|
89
|
+
? { cache: "no-store", query: opts.query, headers: opts.headers }
|
|
90
|
+
: { next: { tags: ["cms", ...(opts.tags ?? [])], revalidate: opts.revalidate ?? 3600 }, query: opts.query, headers: opts.headers });
|
|
91
|
+
try {
|
|
92
|
+
try {
|
|
93
|
+
return ((await get()).data ?? null);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (!isUnavailable(e) || !(await waitForApi()))
|
|
97
|
+
throw e;
|
|
98
|
+
return ((await get()).data ?? null);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (isFetchKitError(e))
|
|
103
|
+
throw new CmsApiError(e.status ?? 0, `CMS API ${e.status ?? e.type}: ${path}`);
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Bỏ giá trị rỗng; mảng → "1,2,3" (API nhận danh sách id phân tách bằng dấu phẩy). */
|
|
108
|
+
const clean = (params) => Object.fromEntries(Object.entries(params)
|
|
109
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
110
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v]));
|
|
111
|
+
/** Thông tin site theo ngôn ngữ của request (hoặc locale truyền vào). */
|
|
112
|
+
export const getSite = async (locale) => cmsFetch("/site", { tags: ["site"], query: clean({ locale: locale ?? (await getRequestLocale()) }) });
|
|
113
|
+
/** Cookie (httpOnly) giữ token mở khoá bài có mật khẩu: "12:token,34:token". */
|
|
114
|
+
export const UNLOCK_COOKIE = "cms-unlock";
|
|
115
|
+
export const UNLOCK_HEADER = "X-CMS-Unlock";
|
|
116
|
+
/** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
|
|
117
|
+
export const resolvePath = (path, query = {}, noStore = false, unlock) => cmsFetch("/resolve", { tags: ["posts"], noStore: noStore || !!unlock, query: clean({ ...query, path }), headers: unlock ? { [UNLOCK_HEADER]: unlock } : undefined });
|
|
118
|
+
export const getPreview = (token) => cmsFetch("/preview", { noStore: true, query: { token } });
|
|
119
|
+
export const getPosts = async ({ withContent, ...q } = {}) => cmsFetch("/posts", { tags: ["posts"], query: clean({ ...q, content: withContent ?? false, locale: q.locale ?? (await getRequestLocale()) }) });
|
|
120
|
+
const loc = async (locale) => locale ?? (await getRequestLocale());
|
|
121
|
+
export const getPost = async (type, slug, locale) => cmsFetch(`/posts/${type}/${encodeURIComponent(slug)}`, { tags: ["posts"], query: clean({ locale: await loc(locale) }) }).catch(() => null);
|
|
122
|
+
export const getTerms = async (taxonomy, hideEmpty = true, locale) => cmsFetch(`/terms/${taxonomy}`, { tags: ["terms"], query: clean({ hideEmpty, locale: await loc(locale) }) });
|
|
123
|
+
/** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
|
|
124
|
+
export const TEMPLATE_DRAFT_COOKIE = "cms-template-draft";
|
|
125
|
+
async function templateDraft() {
|
|
126
|
+
try {
|
|
127
|
+
const { cookies, draftMode } = await import("next/headers");
|
|
128
|
+
if (!(await draftMode()).isEnabled)
|
|
129
|
+
return null;
|
|
130
|
+
return (await cookies()).get(TEMPLATE_DRAFT_COOKIE)?.value ?? null;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Template / phần dựng bằng khối của theme đang dùng (bản sửa trong Trình dựng thắng bản của theme; đang xem trước → bản nháp). */
|
|
137
|
+
export const getTemplates = async () => {
|
|
138
|
+
const draft = await templateDraft();
|
|
139
|
+
return cmsFetch("/templates", draft ? { noStore: true, query: { draft } } : { tags: ["templates"] });
|
|
140
|
+
};
|
|
141
|
+
/** Nội dung mẫu đồng bộ (khối syncedPattern). null = mẫu đã xoá / chưa xuất bản. */
|
|
142
|
+
export const getPattern = (id) => cmsFetch(`/patterns/${id}`, { tags: ["patterns", "posts"] }).catch(() => null);
|
|
143
|
+
export const getMenu = async (location, locale) => cmsFetch(`/menus/${location}`, { tags: ["menus"], query: clean({ locale: await loc(locale) }) });
|
|
144
|
+
export const getMenuById = (id) => cmsFetch(`/menus/id/${id}`, { tags: ["menus"] });
|
|
145
|
+
export const getWidgets = async (area, locale) => cmsFetch(`/widgets/${area}`, { tags: ["widgets"], query: clean({ locale: await loc(locale) }) });
|
|
146
|
+
/** Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận). */
|
|
147
|
+
export const getComments = (postId, page = 1) => cmsFetch(`/comments/${postId}`, { tags: [`comments:${postId}`], query: page > 1 ? { page } : undefined });
|
|
148
|
+
export const getArchives = async (locale) => cmsFetch("/archives", { tags: ["posts"], query: clean({ locale: await loc(locale) }) });
|
|
149
|
+
export const getLanguages = () => cmsFetch("/languages", { tags: ["site"] });
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type CurrentUser } from "./auth-shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dữ liệu thêm của plugin (vd token captcha): mọi ô trong form có name bắt đầu bằng "extra." — gửi lên CMS trong "extra",
|
|
5
|
+
* plugin backend đọc ở action auth.attempt / filter comment.data. Plugin chèn ô qua slot (auth.login.fields, comment_form.fields…).
|
|
6
|
+
*/
|
|
7
|
+
export declare function extraFields(fd: FormData): Record<string, string> | undefined;
|
|
8
|
+
export declare const AUTH_LABELS: {
|
|
9
|
+
login: string;
|
|
10
|
+
password: string;
|
|
11
|
+
submitLogin: string;
|
|
12
|
+
code: string;
|
|
13
|
+
verify: string;
|
|
14
|
+
userName: string;
|
|
15
|
+
email: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
submitRegister: string;
|
|
18
|
+
submitForgot: string;
|
|
19
|
+
newPassword: string;
|
|
20
|
+
submitReset: string;
|
|
21
|
+
sending: string;
|
|
22
|
+
error: string;
|
|
23
|
+
logout: string;
|
|
24
|
+
noAccount: string;
|
|
25
|
+
register: string;
|
|
26
|
+
forgot: string;
|
|
27
|
+
haveAccount: string;
|
|
28
|
+
login2: string;
|
|
29
|
+
registerClosed: string;
|
|
30
|
+
missingToken: string;
|
|
31
|
+
};
|
|
32
|
+
export type AuthLabels = typeof AUTH_LABELS;
|
|
33
|
+
/**
|
|
34
|
+
* Đường dẫn các trang tài khoản. Website nhiều ngôn ngữ truyền bản có tiền tố (localeHref(site, "/login") → "/en/login");
|
|
35
|
+
* template có sẵn của SDK tự truyền.
|
|
36
|
+
*/
|
|
37
|
+
export interface AuthPaths {
|
|
38
|
+
login: string;
|
|
39
|
+
register: string;
|
|
40
|
+
forgotPassword: string;
|
|
41
|
+
home: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const AUTH_PATHS: AuthPaths;
|
|
44
|
+
/**
|
|
45
|
+
* Đăng nhập (có bước mã 2 lớp nếu tài khoản bật). Xong thì chuyển tới ?redirect=… (chỉ đường dẫn nội bộ) hoặc <paramref name="redirectTo"/>.
|
|
46
|
+
*/
|
|
47
|
+
/** fields: ô thêm của plugin (captcha…), thường là <Slot name="auth.login.fields" /> — template có sẵn của SDK tự truyền. */
|
|
48
|
+
export declare function LoginForm({ redirectTo, labels, links, paths, fields }: {
|
|
49
|
+
redirectTo?: string;
|
|
50
|
+
labels?: Partial<AuthLabels>;
|
|
51
|
+
links?: boolean;
|
|
52
|
+
paths?: Partial<AuthPaths>;
|
|
53
|
+
fields?: ReactNode;
|
|
54
|
+
}): import("react").JSX.Element;
|
|
55
|
+
/** Đăng ký thành viên (chỉ khi quản trị viên bật "Ai cũng có thể đăng ký"). Vai trò = vai trò mặc định trong Cài đặt. */
|
|
56
|
+
export declare function RegisterForm({ labels, links, paths, fields }: {
|
|
57
|
+
labels?: Partial<AuthLabels>;
|
|
58
|
+
links?: boolean;
|
|
59
|
+
paths?: Partial<AuthPaths>;
|
|
60
|
+
fields?: ReactNode;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
62
|
+
/** Gửi email đặt lại mật khẩu (liên kết trỏ về /reset-password của website). */
|
|
63
|
+
export declare function ForgotPasswordForm({ labels, paths, fields }: {
|
|
64
|
+
labels?: Partial<AuthLabels>;
|
|
65
|
+
paths?: Partial<AuthPaths>;
|
|
66
|
+
fields?: ReactNode;
|
|
67
|
+
}): import("react").JSX.Element;
|
|
68
|
+
/** Đặt mật khẩu mới từ liên kết trong email (?token=…). */
|
|
69
|
+
export declare function ResetPasswordForm({ labels, paths }: {
|
|
70
|
+
labels?: Partial<AuthLabels>;
|
|
71
|
+
paths?: Partial<AuthPaths>;
|
|
72
|
+
}): import("react").JSX.Element;
|
|
73
|
+
/** Nút đăng xuất: thu hồi phiên ở CMS, xoá cookie rồi chuyển về <paramref name="redirectTo"/>. */
|
|
74
|
+
export declare function LogoutButton({ redirectTo, className, children }: {
|
|
75
|
+
redirectTo?: string;
|
|
76
|
+
className?: string;
|
|
77
|
+
children?: ReactNode;
|
|
78
|
+
}): import("react").JSX.Element;
|
|
79
|
+
/**
|
|
80
|
+
* Người đang đăng nhập, dùng trong component client (null = khách, undefined = đang tải).
|
|
81
|
+
* Server Component nên dùng getCurrentUser() để khỏi tốn thêm một request.
|
|
82
|
+
*/
|
|
83
|
+
export declare function useCurrentUser(): CurrentUser | null | undefined;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { createFetchKit, isFetchKitError } from "next-fetch-kit";
|
|
5
|
+
import { safeRedirect } from "./auth-shared.js";
|
|
6
|
+
/**
|
|
7
|
+
* Form đăng nhập / đăng ký / quên mật khẩu của website. Gửi qua /api/cms/auth/* của frontend core —
|
|
8
|
+
* token nằm trong cookie httpOnly, JavaScript của trang không bao giờ thấy.
|
|
9
|
+
* Theme đặt các form này vào template riêng (login.tsx, register.tsx…) hoặc dùng trang có sẵn.
|
|
10
|
+
*/
|
|
11
|
+
const authApi = createFetchKit({ baseURL: "/api/cms/auth", timeout: 15_000 });
|
|
12
|
+
function errorMessage(e, fallback) {
|
|
13
|
+
if (isFetchKitError(e)) {
|
|
14
|
+
const d = e.data;
|
|
15
|
+
if (d?.errors)
|
|
16
|
+
return Object.values(d.errors).flat().join(". ");
|
|
17
|
+
return d?.title ?? fallback;
|
|
18
|
+
}
|
|
19
|
+
return e?.message ?? fallback;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dữ liệu thêm của plugin (vd token captcha): mọi ô trong form có name bắt đầu bằng "extra." — gửi lên CMS trong "extra",
|
|
23
|
+
* plugin backend đọc ở action auth.attempt / filter comment.data. Plugin chèn ô qua slot (auth.login.fields, comment_form.fields…).
|
|
24
|
+
*/
|
|
25
|
+
export function extraFields(fd) {
|
|
26
|
+
const extra = {};
|
|
27
|
+
for (const [k, v] of fd.entries())
|
|
28
|
+
if (k.startsWith("extra.") && typeof v === "string")
|
|
29
|
+
extra[k.slice(6)] = v;
|
|
30
|
+
return Object.keys(extra).length ? extra : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Tham số trên URL hiện tại (đọc sau khi hiển thị — trang có thể đã được cache tĩnh). */
|
|
33
|
+
function useQueryParam(name) {
|
|
34
|
+
const [value, setValue] = useState(null);
|
|
35
|
+
useEffect(() => { setValue(new URLSearchParams(window.location.search).get(name)); }, [name]);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export const AUTH_LABELS = {
|
|
39
|
+
login: "Tên đăng nhập hoặc email", password: "Mật khẩu", submitLogin: "Đăng nhập", code: "Mã xác thực (6 số)",
|
|
40
|
+
verify: "Xác nhận", userName: "Tên đăng nhập", email: "Email", displayName: "Tên hiển thị", submitRegister: "Tạo tài khoản",
|
|
41
|
+
submitForgot: "Gửi liên kết đặt lại", newPassword: "Mật khẩu mới (tối thiểu 10 ký tự)", submitReset: "Đặt mật khẩu",
|
|
42
|
+
sending: "Đang xử lý…", error: "Có lỗi xảy ra, vui lòng thử lại", logout: "Đăng xuất",
|
|
43
|
+
noAccount: "Chưa có tài khoản?", register: "Đăng ký", forgot: "Quên mật khẩu?", haveAccount: "Đã có tài khoản?", login2: "Đăng nhập",
|
|
44
|
+
registerClosed: "Website hiện không mở đăng ký thành viên.", missingToken: "Liên kết không hợp lệ — hãy yêu cầu đặt lại mật khẩu lần nữa.",
|
|
45
|
+
};
|
|
46
|
+
export const AUTH_PATHS = { login: "/login", register: "/register", forgotPassword: "/forgot-password", home: "/" };
|
|
47
|
+
function Field({ label, children }) {
|
|
48
|
+
return _jsx("p", { className: "auth-field", children: _jsxs("label", { children: [label, children] }) });
|
|
49
|
+
}
|
|
50
|
+
function Status({ error, message }) {
|
|
51
|
+
if (error)
|
|
52
|
+
return _jsx("p", { className: "form-error", role: "alert", children: error });
|
|
53
|
+
if (message)
|
|
54
|
+
return _jsx("p", { className: "form-success", role: "status", children: message });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Đăng nhập (có bước mã 2 lớp nếu tài khoản bật). Xong thì chuyển tới ?redirect=… (chỉ đường dẫn nội bộ) hoặc <paramref name="redirectTo"/>.
|
|
59
|
+
*/
|
|
60
|
+
/** fields: ô thêm của plugin (captcha…), thường là <Slot name="auth.login.fields" /> — template có sẵn của SDK tự truyền. */
|
|
61
|
+
export function LoginForm({ redirectTo, labels, links = true, paths, fields }) {
|
|
62
|
+
const L = { ...AUTH_LABELS, ...labels };
|
|
63
|
+
const P = { ...AUTH_PATHS, ...paths };
|
|
64
|
+
const redirectParam = useQueryParam("redirect");
|
|
65
|
+
const [challenge, setChallenge] = useState(null);
|
|
66
|
+
const [busy, setBusy] = useState(false);
|
|
67
|
+
const [error, setError] = useState("");
|
|
68
|
+
async function onSubmit(e) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
const fd = new FormData(e.currentTarget);
|
|
71
|
+
setBusy(true);
|
|
72
|
+
setError("");
|
|
73
|
+
try {
|
|
74
|
+
const { data } = challenge
|
|
75
|
+
? await authApi.post("/2fa", { body: { challenge, code: fd.get("code") } })
|
|
76
|
+
: await authApi.post("/login", { body: { login: fd.get("login"), password: fd.get("password"), extra: extraFields(fd) } });
|
|
77
|
+
if (data.twoFactorRequired && data.challenge) {
|
|
78
|
+
setChallenge(data.challenge);
|
|
79
|
+
setBusy(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Tải lại hẳn trang đích: mọi Server Component đọc được cookie phiên mới
|
|
83
|
+
window.location.assign(safeRedirect(redirectParam, redirectTo ?? P.home));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
setError(errorMessage(err, L.error));
|
|
87
|
+
setBusy(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return (_jsxs("form", { className: "auth-form login-form", onSubmit: onSubmit, children: [challenge ? (_jsx(Field, { label: L.code, children: _jsx("input", { name: "code", inputMode: "numeric", autoComplete: "one-time-code", required: true, maxLength: 12, autoFocus: true }) })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: L.login, children: _jsx("input", { name: "login", autoComplete: "username", required: true, maxLength: 200 }) }), _jsx(Field, { label: L.password, children: _jsx("input", { name: "password", type: "password", autoComplete: "current-password", required: true }) }), fields] })), _jsx("p", { children: _jsx("button", { type: "submit", disabled: busy, children: busy ? L.sending : challenge ? L.verify : L.submitLogin }) }), _jsx(Status, { error: error }), links && !challenge && (_jsxs("p", { className: "auth-links", children: [_jsx("a", { href: P.forgotPassword, children: L.forgot }), " \u00B7 ", L.noAccount, " ", _jsx("a", { href: P.register, children: L.register })] }))] }));
|
|
91
|
+
}
|
|
92
|
+
/** Đăng ký thành viên (chỉ khi quản trị viên bật "Ai cũng có thể đăng ký"). Vai trò = vai trò mặc định trong Cài đặt. */
|
|
93
|
+
export function RegisterForm({ labels, links = true, paths, fields }) {
|
|
94
|
+
const L = { ...AUTH_LABELS, ...labels };
|
|
95
|
+
const P = { ...AUTH_PATHS, ...paths };
|
|
96
|
+
const [open, setOpen] = useState(null);
|
|
97
|
+
const [busy, setBusy] = useState(false);
|
|
98
|
+
const [error, setError] = useState("");
|
|
99
|
+
const [message, setMessage] = useState("");
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
authApi.get("/options").then(({ data }) => setOpen(data.canRegister)).catch(() => setOpen(false));
|
|
102
|
+
}, []);
|
|
103
|
+
async function onSubmit(e) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
const form = e.currentTarget;
|
|
106
|
+
const fd = new FormData(form);
|
|
107
|
+
setBusy(true);
|
|
108
|
+
setError("");
|
|
109
|
+
setMessage("");
|
|
110
|
+
try {
|
|
111
|
+
const { data } = await authApi.post("/register", {
|
|
112
|
+
body: { userName: fd.get("userName"), email: fd.get("email"), password: fd.get("password"), displayName: fd.get("displayName") || null, extra: extraFields(fd) },
|
|
113
|
+
});
|
|
114
|
+
form.reset();
|
|
115
|
+
setMessage(data.message);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
setError(errorMessage(err, L.error));
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
setBusy(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (open === false)
|
|
125
|
+
return _jsx("p", { className: "auth-form register-closed", children: L.registerClosed });
|
|
126
|
+
return (_jsxs("form", { className: "auth-form register-form", onSubmit: onSubmit, children: [_jsx(Field, { label: L.userName, children: _jsx("input", { name: "userName", autoComplete: "username", required: true, minLength: 3, maxLength: 40, pattern: "[a-zA-Z0-9._\\-]+" }) }), _jsx(Field, { label: L.email, children: _jsx("input", { name: "email", type: "email", autoComplete: "email", required: true, maxLength: 200 }) }), _jsx(Field, { label: L.displayName, children: _jsx("input", { name: "displayName", autoComplete: "name", maxLength: 100 }) }), _jsx(Field, { label: L.password, children: _jsx("input", { name: "password", type: "password", autoComplete: "new-password", required: true, minLength: 10 }) }), fields, _jsx("p", { children: _jsx("button", { type: "submit", disabled: busy || open === null, children: busy ? L.sending : L.submitRegister }) }), _jsx(Status, { error: error, message: message }), links && _jsxs("p", { className: "auth-links", children: [L.haveAccount, " ", _jsx("a", { href: P.login, children: L.login2 })] })] }));
|
|
127
|
+
}
|
|
128
|
+
/** Gửi email đặt lại mật khẩu (liên kết trỏ về /reset-password của website). */
|
|
129
|
+
export function ForgotPasswordForm({ labels, paths, fields }) {
|
|
130
|
+
const L = { ...AUTH_LABELS, ...labels };
|
|
131
|
+
const P = { ...AUTH_PATHS, ...paths };
|
|
132
|
+
const [busy, setBusy] = useState(false);
|
|
133
|
+
const [error, setError] = useState("");
|
|
134
|
+
const [message, setMessage] = useState("");
|
|
135
|
+
async function onSubmit(e) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
const fd = new FormData(e.currentTarget);
|
|
138
|
+
setBusy(true);
|
|
139
|
+
setError("");
|
|
140
|
+
setMessage("");
|
|
141
|
+
try {
|
|
142
|
+
const { data } = await authApi.post("/forgot-password", { body: { login: fd.get("login"), extra: extraFields(fd) } });
|
|
143
|
+
setMessage(data.message);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
setError(errorMessage(err, L.error));
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
setBusy(false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return (_jsxs("form", { className: "auth-form forgot-form", onSubmit: onSubmit, children: [_jsx(Field, { label: L.login, children: _jsx("input", { name: "login", autoComplete: "username", required: true, maxLength: 200 }) }), fields, _jsx("p", { children: _jsx("button", { type: "submit", disabled: busy, children: busy ? L.sending : L.submitForgot }) }), _jsx(Status, { error: error, message: message }), _jsx("p", { className: "auth-links", children: _jsx("a", { href: P.login, children: L.login2 }) })] }));
|
|
153
|
+
}
|
|
154
|
+
/** Đặt mật khẩu mới từ liên kết trong email (?token=…). */
|
|
155
|
+
export function ResetPasswordForm({ labels, paths }) {
|
|
156
|
+
const L = { ...AUTH_LABELS, ...labels };
|
|
157
|
+
const P = { ...AUTH_PATHS, ...paths };
|
|
158
|
+
const token = useQueryParam("token");
|
|
159
|
+
const [busy, setBusy] = useState(false);
|
|
160
|
+
const [error, setError] = useState("");
|
|
161
|
+
const [message, setMessage] = useState("");
|
|
162
|
+
async function onSubmit(e) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const fd = new FormData(e.currentTarget);
|
|
165
|
+
setBusy(true);
|
|
166
|
+
setError("");
|
|
167
|
+
setMessage("");
|
|
168
|
+
try {
|
|
169
|
+
const { data } = await authApi.post("/reset-password", { body: { token, password: fd.get("password") } });
|
|
170
|
+
setMessage(data.message);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
setError(errorMessage(err, L.error));
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
setBusy(false);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (token === "")
|
|
180
|
+
return _jsx("p", { className: "form-error", children: L.missingToken });
|
|
181
|
+
return (_jsxs("form", { className: "auth-form reset-form", onSubmit: onSubmit, children: [_jsx(Field, { label: L.newPassword, children: _jsx("input", { name: "password", type: "password", autoComplete: "new-password", required: true, minLength: 10 }) }), _jsx("p", { children: _jsx("button", { type: "submit", disabled: busy || !token || !!message, children: busy ? L.sending : L.submitReset }) }), _jsx(Status, { error: error, message: message }), message && _jsx("p", { className: "auth-links", children: _jsx("a", { href: P.login, children: L.login2 }) })] }));
|
|
182
|
+
}
|
|
183
|
+
/** Nút đăng xuất: thu hồi phiên ở CMS, xoá cookie rồi chuyển về <paramref name="redirectTo"/>. */
|
|
184
|
+
export function LogoutButton({ redirectTo = "/", className, children }) {
|
|
185
|
+
const [busy, setBusy] = useState(false);
|
|
186
|
+
return (_jsx("button", { type: "button", className: className ?? "logout-button", disabled: busy, onClick: async () => {
|
|
187
|
+
setBusy(true);
|
|
188
|
+
try {
|
|
189
|
+
await authApi.post("/logout");
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
window.location.assign(safeRedirect(redirectTo));
|
|
193
|
+
}
|
|
194
|
+
}, children: children ?? AUTH_LABELS.logout }));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Người đang đăng nhập, dùng trong component client (null = khách, undefined = đang tải).
|
|
198
|
+
* Server Component nên dùng getCurrentUser() để khỏi tốn thêm một request.
|
|
199
|
+
*/
|
|
200
|
+
export function useCurrentUser() {
|
|
201
|
+
const [user, setUser] = useState(undefined);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
authApi.get("/me").then(({ data }) => setUser(data.user)).catch(() => setUser(null));
|
|
204
|
+
}, []);
|
|
205
|
+
return user;
|
|
206
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TemplateProps } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Template mặc định cho /login, /register, /forgot-password, /reset-password khi theme không có template
|
|
4
|
+
* "login" | "register" | "forgot-password" | "reset-password" hoặc "auth". Hiển thị trong layout của theme.
|
|
5
|
+
*/
|
|
6
|
+
export default function AuthPage({ data, site }: TemplateProps): Promise<import("react").JSX.Element>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getCurrentUser } from "./auth.js";
|
|
3
|
+
import { ForgotPasswordForm, LoginForm, LogoutButton, RegisterForm, ResetPasswordForm } from "./auth-forms.js";
|
|
4
|
+
import { Slot } from "./components.js";
|
|
5
|
+
import { getT } from "./i18n.js";
|
|
6
|
+
import { localeHref } from "./locale.js";
|
|
7
|
+
/**
|
|
8
|
+
* Template mặc định cho /login, /register, /forgot-password, /reset-password khi theme không có template
|
|
9
|
+
* "login" | "register" | "forgot-password" | "reset-password" hoặc "auth". Hiển thị trong layout của theme.
|
|
10
|
+
*/
|
|
11
|
+
export default async function AuthPage({ data, site }) {
|
|
12
|
+
const t = await getT();
|
|
13
|
+
const name = data.route?.name ?? "login";
|
|
14
|
+
const L = Object.fromEntries(Object.keys(LABEL_KEYS).map((k) => [k, t(LABEL_KEYS[k])]));
|
|
15
|
+
const user = name === "login" || name === "register" ? await getCurrentUser() : null;
|
|
16
|
+
// Đường dẫn theo ngôn ngữ đang xem (/en/login…)
|
|
17
|
+
const paths = {
|
|
18
|
+
login: localeHref(site, "/login"), register: localeHref(site, "/register"),
|
|
19
|
+
forgotPassword: localeHref(site, "/forgot-password"), home: localeHref(site, "/"),
|
|
20
|
+
};
|
|
21
|
+
return (_jsxs("main", { className: "auth-page", children: [_jsx("h1", { children: data.title }), user ? (_jsxs("p", { className: "auth-signed-in", children: [t("auth.signedInAs", { name: user.displayName }), " ", _jsx(LogoutButton, { redirectTo: paths.home, children: L.logout })] })) : name === "register" ? _jsx(RegisterForm, { labels: L, paths: paths, fields: _jsx(Slot, { name: "auth.register.fields" }) })
|
|
22
|
+
: name === "forgot-password" ? _jsx(ForgotPasswordForm, { labels: L, paths: paths, fields: _jsx(Slot, { name: "auth.forgot_password.fields" }) })
|
|
23
|
+
: name === "reset-password" ? _jsx(ResetPasswordForm, { labels: L, paths: paths })
|
|
24
|
+
: _jsx(LoginForm, { labels: L, paths: paths, fields: _jsx(Slot, { name: "auth.login.fields" }) })] }));
|
|
25
|
+
}
|
|
26
|
+
/** Khoá dịch (locales/*.json của theme ghi đè được) cho từng nhãn của form. */
|
|
27
|
+
const LABEL_KEYS = {
|
|
28
|
+
login: "auth.login", password: "auth.password", submitLogin: "auth.submitLogin", code: "auth.code", verify: "auth.verify",
|
|
29
|
+
userName: "auth.userName", email: "auth.email", displayName: "auth.displayName", submitRegister: "auth.submitRegister",
|
|
30
|
+
submitForgot: "auth.submitForgot", newPassword: "auth.newPassword", submitReset: "auth.submitReset", sending: "auth.sending",
|
|
31
|
+
error: "auth.error", logout: "auth.logout", noAccount: "auth.noAccount", register: "auth.register", forgot: "auth.forgot",
|
|
32
|
+
haveAccount: "auth.haveAccount", login2: "auth.login2", registerClosed: "auth.registerClosed", missingToken: "auth.missingToken",
|
|
33
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Tên cookie phiên đăng nhập của website (httpOnly, do frontend core đặt). Dùng chung cho proxy và SDK. */
|
|
2
|
+
export declare const ACCESS_COOKIE = "cms_at";
|
|
3
|
+
export declare const REFRESH_COOKIE = "cms_rt";
|
|
4
|
+
/** Người dùng đang đăng nhập trên website. */
|
|
5
|
+
export interface CurrentUser {
|
|
6
|
+
id: number;
|
|
7
|
+
email: string;
|
|
8
|
+
userName: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
roles: string[];
|
|
11
|
+
capabilities: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Chỉ nhận đường dẫn nội bộ ("/account") — chặn chuyển hướng ra trang ngoài (?redirect=https://lừa-đảo). */
|
|
14
|
+
export declare function safeRedirect(target: string | null | undefined, fallback?: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Tên cookie phiên đăng nhập của website (httpOnly, do frontend core đặt). Dùng chung cho proxy và SDK. */
|
|
2
|
+
export const ACCESS_COOKIE = "cms_at";
|
|
3
|
+
export const REFRESH_COOKIE = "cms_rt";
|
|
4
|
+
/** Chỉ nhận đường dẫn nội bộ ("/account") — chặn chuyển hướng ra trang ngoài (?redirect=https://lừa-đảo). */
|
|
5
|
+
export function safeRedirect(target, fallback = "/") {
|
|
6
|
+
return target && target.startsWith("/") && !target.startsWith("//") && !target.startsWith("/\\") ? target : fallback;
|
|
7
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CurrentUser } from "./auth-shared.js";
|
|
2
|
+
export { ACCESS_COOKIE, REFRESH_COOKIE, safeRedirect, type CurrentUser } from "./auth-shared.js";
|
|
3
|
+
/** Access token của người đang xem (server). Trang gọi hàm này sẽ render theo từng người (không cache tĩnh). */
|
|
4
|
+
export declare const getAccessToken: () => Promise<string | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* Người đang đăng nhập trên website, null nếu là khách.
|
|
7
|
+
* Dùng trong Server Component, route, generateMetadata. Component client: dùng useCurrentUser() hoặc nhận qua props.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getCurrentUser: () => Promise<CurrentUser | null>;
|
|
10
|
+
/** Người dùng có quyền này không — current_user_can(). */
|
|
11
|
+
export declare function userCan(user: CurrentUser | null | undefined, capability: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Bắt buộc đăng nhập: khách bị chuyển tới /login, đăng nhập xong quay lại <paramref name="returnTo"/>.
|
|
14
|
+
* const user = await requireUser("/account");
|
|
15
|
+
*/
|
|
16
|
+
export declare function requireUser(returnTo?: string): Promise<CurrentUser>;
|