@mapomodule/core 1.0.0 → 2.0.0-beta.1

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 (41) hide show
  1. package/README.md +46 -0
  2. package/dist/index.d.cts +1 -0
  3. package/dist/index.d.mts +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.mjs +21 -0
  6. package/dist/module.d.cts +2 -0
  7. package/dist/module.d.mts +2 -0
  8. package/dist/module.d.ts +2 -0
  9. package/dist/module.json +9 -0
  10. package/dist/module.mjs +20 -0
  11. package/dist/types.d.mts +13 -0
  12. package/package.json +58 -15
  13. package/src/index.ts +4 -0
  14. package/src/module.ts +102 -0
  15. package/src/runtime/__tests__/plugins/00.fetch.spec.ts +144 -0
  16. package/src/runtime/api/crud.ts +130 -0
  17. package/src/runtime/api/multipart.ts +78 -0
  18. package/src/runtime/auth/useCanAccessRoute.ts +48 -0
  19. package/src/runtime/auth/useMapoAuth.ts +91 -0
  20. package/src/runtime/composables/useMapo.ts +61 -0
  21. package/src/runtime/middleware/auth.ts +24 -0
  22. package/src/runtime/middleware/permissions.ts +52 -0
  23. package/src/runtime/middleware/roles.ts +23 -0
  24. package/src/runtime/page-meta.nuxt.d.ts +23 -0
  25. package/src/runtime/plugins/00.fetch.ts +85 -0
  26. package/src/runtime/plugins/01.init.server.ts +62 -0
  27. package/src/runtime/types.nuxt.d.ts +18 -0
  28. package/src/runtime/types.ts +88 -0
  29. package/src/runtime/utils/useMapoFetch.ts +41 -0
  30. package/src/types.ts +13 -0
  31. package/CHANGELOG.md +0 -248
  32. package/api/crud.js +0 -141
  33. package/api/multipart.js +0 -41
  34. package/index.js +0 -134
  35. package/interceptor/auth.js +0 -26
  36. package/interceptor/index.js +0 -5
  37. package/middleware/auth.js +0 -10
  38. package/middleware/permissions.js +0 -36
  39. package/middleware/roles.js +0 -10
  40. package/module.js +0 -44
  41. package/templates/options.js +0 -1
@@ -0,0 +1,48 @@
1
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
2
+ import type { RouteMeta } from "vue-router";
3
+
4
+ /** Two-format permission spec: Django model shorthand or explicit codename list. */
5
+ export type RoutePermissions = { model: string } | string[];
6
+
7
+ /**
8
+ * Returns whether the current user can access a route based on its meta.
9
+ * Mirrors the logic of the `auth`, `permissions`, and `roles` middlewares
10
+ * so the sidebar and any UI that gates visibility stays in sync with navigation guards.
11
+ */
12
+ export function useCanAccessRoute(meta: RouteMeta): boolean {
13
+ const auth = useAuthStore();
14
+
15
+ const raw = meta.middleware;
16
+ const middleware = Array.isArray(raw) ? raw : raw ? [raw] : [];
17
+
18
+ // auth middleware: must be logged in
19
+ if (middleware.includes("auth") && !auth.isAuthenticated) return false;
20
+
21
+ // roles middleware
22
+ if (middleware.includes("roles")) {
23
+ const required = meta.roles as string[] | undefined;
24
+ if (required?.length && !auth.info?.is_superuser) {
25
+ const userGroups = auth.info?.groups ?? [];
26
+ if (!required.some((r: string) => userGroups.includes(r))) return false;
27
+ }
28
+ }
29
+
30
+ // permissions middleware
31
+ if (middleware.includes("permissions")) {
32
+ const perms = meta.permissions as RoutePermissions | undefined;
33
+ if (perms && !auth.info?.is_superuser) {
34
+ if (Array.isArray(perms)) {
35
+ if (!perms.every((p) => auth.rawPermissions.includes(p))) return false;
36
+ } else {
37
+ const hasView = auth.rawPermissions.some(
38
+ (p) =>
39
+ p === `view_${perms.model}` ||
40
+ (p.endsWith(`_${perms.model}`) && p.startsWith("view")),
41
+ );
42
+ if (!hasView) return false;
43
+ }
44
+ }
45
+ }
46
+
47
+ return true;
48
+ }
@@ -0,0 +1,91 @@
1
+ import { useNuxtApp, navigateTo, useCookie, useRuntimeConfig } from "nuxt/app";
2
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
3
+ import { CoreCookieEnum } from "../types";
4
+ import type { Credentials, MapoCoreRuntimeConfig } from "../types";
5
+
6
+ /**
7
+ * Optional endpoint overrides for auth operations.
8
+ */
9
+ type AuthOverrides = {
10
+ /** Login endpoint used to create the server session. */
11
+ authLoginUrl?: string;
12
+ /** Endpoint used to fetch the current authenticated user profile. */
13
+ userInfoApi?: string;
14
+ /** Logout endpoint used to invalidate the server session. */
15
+ logoutUrl?: string;
16
+ };
17
+
18
+ /**
19
+ * Provides authentication helpers built on top of the shared Mapo fetch client.
20
+ *
21
+ * Endpoints are resolved from runtime config and can be selectively overridden
22
+ * through `options`.
23
+ *
24
+ * @param options Optional endpoint overrides for login, logout, and user fetch.
25
+ * @returns Auth helpers: `login`, `logout`, and `fetchUser`.
26
+ */
27
+ export function useMapoAuth(options?: AuthOverrides) {
28
+ const rc = useRuntimeConfig().public.mapoCore as MapoCoreRuntimeConfig;
29
+ const authLoginUrl = options?.authLoginUrl ?? rc.authLoginUrl;
30
+ const userInfoApi = options?.userInfoApi ?? rc.userInfoApi;
31
+ const logoutUrl = options?.logoutUrl ?? rc.logoutUrl;
32
+ const loginUrl = rc.loginUrl;
33
+
34
+ const { $mapoFetch } = useNuxtApp() as ReturnType<typeof useNuxtApp> & {
35
+ $mapoFetch: typeof $fetch;
36
+ };
37
+ const authStore = useAuthStore();
38
+
39
+ /**
40
+ * Fetches the current user from `userInfoApi` and hydrates the auth store.
41
+ */
42
+ async function fetchUser(): Promise<void> {
43
+ const user = await $mapoFetch<
44
+ import("@mapomodule/store/runtime/types").MapoUser
45
+ >(userInfoApi, { method: "GET" });
46
+ authStore.setUser(user);
47
+ }
48
+
49
+ /**
50
+ * Performs login and then confirms the session by fetching the current user.
51
+ *
52
+ * The backend is expected to set an HttpOnly session cookie on successful
53
+ * login. `fetchUser` is used immediately after to hydrate store state.
54
+ *
55
+ * @param credentials User credentials payload for the login endpoint.
56
+ */
57
+ async function login(credentials: Credentials): Promise<void> {
58
+ // The backend sets the session cookie (HttpOnly) on a successful login.
59
+ // We confirm the session by fetching the user — if it succeeds, the
60
+ // store is hydrated and isAuthenticated becomes true.
61
+ await $mapoFetch(authLoginUrl, { method: "POST", body: credentials });
62
+ await fetchUser();
63
+ }
64
+
65
+ /**
66
+ * Logs out the current user and always performs local cleanup.
67
+ *
68
+ * Cleanup includes clearing the session cookie mirror, resetting auth store
69
+ * state, removing persisted Mapo draft keys from localStorage, and navigating
70
+ * to the configured login page.
71
+ */
72
+ async function logout(): Promise<void> {
73
+ try {
74
+ await $mapoFetch(logoutUrl, { method: "GET" });
75
+ } finally {
76
+ useCookie(CoreCookieEnum.Session).value = null;
77
+ authStore.reset();
78
+ if (typeof localStorage !== "undefined") {
79
+ const toRemove: string[] = [];
80
+ for (let i = 0; i < localStorage.length; i++) {
81
+ const k = localStorage.key(i);
82
+ if (k?.startsWith("mapo:draft:")) toRemove.push(k);
83
+ }
84
+ toRemove.forEach((k) => localStorage.removeItem(k));
85
+ }
86
+ await navigateTo(loginUrl);
87
+ }
88
+ }
89
+
90
+ return { login, logout, fetchUser };
91
+ }
@@ -0,0 +1,61 @@
1
+ import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
2
+ import { useConfirmStore } from "@mapomodule/store/runtime/stores/confirm";
3
+ import { useMapoAuth } from "../auth/useMapoAuth";
4
+ import { useCrud } from "../api/crud";
5
+ import type { MapoOptions } from "../../types";
6
+ import type { CrudRepository } from "../types";
7
+
8
+ /**
9
+ * API layer exposed by {@link useMapo}.
10
+ */
11
+ export interface MapoApiLayer {
12
+ /**
13
+ * Creates a CRUD repository bound to the provided endpoint.
14
+ *
15
+ * @param endpoint API endpoint for the resource collection.
16
+ * @returns A typed repository with list/detail/create/update/delete helpers.
17
+ */
18
+ crud<T>(endpoint: string): CrudRepository<T>;
19
+ }
20
+
21
+ /**
22
+ * High-level facade that groups the most common Mapo runtime capabilities.
23
+ *
24
+ * This keeps feature access ergonomic in components by exposing API, auth,
25
+ * feedback stores, and resolved options from a single composable call.
26
+ */
27
+ export interface MapoFacade {
28
+ /** Resource-oriented API helpers. */
29
+ $api: MapoApiLayer;
30
+ /** Authentication helpers (`login`, `logout`, `fetchUser`). */
31
+ $auth: ReturnType<typeof useMapoAuth>;
32
+ /** Global snackbar store for user feedback messages. */
33
+ $snack: ReturnType<typeof useSnackStore>;
34
+ /** Global confirm dialog store for destructive/critical actions. */
35
+ $confirm: ReturnType<typeof useConfirmStore>;
36
+ /** Effective options passed to the facade factory. */
37
+ $options: MapoOptions;
38
+ }
39
+
40
+ /**
41
+ * Creates the Mapo facade used across admin pages and composables.
42
+ *
43
+ * The facade is a convenience aggregator over lower-level composables/stores:
44
+ * - `$api.crud<T>(endpoint)` delegates to `useCrud<T>(endpoint)`.
45
+ * - `$auth` delegates to `useMapoAuth(options)` (same overrides).
46
+ * - `$snack` and `$confirm` expose the shared UI feedback stores.
47
+ *
48
+ * @param options Optional runtime overrides forwarded to the auth layer.
49
+ * @returns A `MapoFacade` with grouped API/auth/feedback capabilities.
50
+ */
51
+ export function useMapo(options: MapoOptions = {}): MapoFacade {
52
+ return {
53
+ $api: {
54
+ crud: <T>(endpoint: string): CrudRepository<T> => useCrud<T>(endpoint),
55
+ },
56
+ $auth: useMapoAuth(options),
57
+ $snack: useSnackStore(),
58
+ $confirm: useConfirmStore(),
59
+ $options: options,
60
+ };
61
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ defineNuxtRouteMiddleware,
3
+ navigateTo,
4
+ useRuntimeConfig,
5
+ } from "nuxt/app";
6
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
7
+
8
+ /**
9
+ * Route middleware that protects authenticated pages.
10
+ *
11
+ * If the user is not authenticated, it redirects to the configured login page
12
+ * and appends the current route (`to.fullPath`) as a `redirect` query param,
13
+ * so the app can return the user to the original destination after login.
14
+ */
15
+ export default defineNuxtRouteMiddleware((to) => {
16
+ const loginUrl = (useRuntimeConfig().public.mapoCore as { loginUrl: string })
17
+ .loginUrl;
18
+ const auth = useAuthStore();
19
+ if (!auth.isAuthenticated) {
20
+ return navigateTo(
21
+ `${loginUrl}?redirect=${encodeURIComponent(to.fullPath)}`,
22
+ );
23
+ }
24
+ });
@@ -0,0 +1,52 @@
1
+ import { defineNuxtRouteMiddleware, createError } from "nuxt/app";
2
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
3
+
4
+ type RoutePermissions = { model: string } | string[];
5
+
6
+ /**
7
+ * Route middleware that enforces page-level permissions from `to.meta.permissions`.
8
+ *
9
+ * Supported formats:
10
+ * - `string[]`: all listed raw permission codenames are required.
11
+ * - `{ model: string }`: permissions are derived from codenames ending with
12
+ * `_<model>` (for example, `view_article`, `change_article`).
13
+ *
14
+ * Behavior:
15
+ * - If no permissions are declared, access is allowed.
16
+ * - Superusers always pass; for model-based permissions, full CRUD actions are
17
+ * exposed in `auth.pagePermissions[routeName]`.
18
+ * - For regular users, access is denied with HTTP 403 when required permissions
19
+ * are missing (or `view` is missing for model-based permissions).
20
+ * - For allowed model-based routes, resolved actions are stored in
21
+ * `auth.pagePermissions[routeName]`.
22
+ */
23
+ export default defineNuxtRouteMiddleware((to) => {
24
+ const auth = useAuthStore();
25
+ const perms = to.meta.permissions as RoutePermissions | undefined;
26
+ if (!perms) return;
27
+ if (auth.info?.is_superuser) {
28
+ if (!Array.isArray(perms)) {
29
+ auth.pagePermissions[String(to.name)] = [
30
+ "view",
31
+ "add",
32
+ "change",
33
+ "delete",
34
+ ];
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (Array.isArray(perms)) {
40
+ if (!perms.every((p) => auth.rawPermissions.includes(p)))
41
+ throw createError({ statusCode: 403, message: "Forbidden" });
42
+ } else {
43
+ const actions = auth.rawPermissions
44
+ .filter((p) => p.endsWith(`_${perms.model}`))
45
+ .map((p) => p.replace(`_${perms.model}`, ""));
46
+
47
+ if (!actions.includes("view"))
48
+ throw createError({ statusCode: 403, message: "Forbidden" });
49
+
50
+ auth.pagePermissions[String(to.name)] = actions;
51
+ }
52
+ });
@@ -0,0 +1,23 @@
1
+ import { defineNuxtRouteMiddleware, createError } from "nuxt/app";
2
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
3
+
4
+ /**
5
+ * Route middleware that enforces role-based access from `to.meta.roles`.
6
+ *
7
+ * Access rules:
8
+ * - If no roles are configured on the route, access is allowed.
9
+ * - Superusers always bypass role checks.
10
+ * - For other users, at least one required role must match a user group.
11
+ * - If no match is found, navigation is blocked with HTTP 403.
12
+ */
13
+ export default defineNuxtRouteMiddleware((to) => {
14
+ const auth = useAuthStore();
15
+ const required = to.meta.roles as string[] | undefined;
16
+ if (!required?.length) return;
17
+ if (auth.info?.is_superuser) return;
18
+ const userGroups = auth.info?.groups ?? [];
19
+ const hasRole = required.some((r) => userGroups.includes(r));
20
+ if (!hasRole) {
21
+ throw createError({ statusCode: 403, message: "Forbidden" });
22
+ }
23
+ });
@@ -0,0 +1,23 @@
1
+ declare module "#app" {
2
+ interface PageMeta {
3
+ /**
4
+ * Permissions required to access this route. Checked by the 'permissions' middleware and sidebar visibility.
5
+ * - `{ model: string }` — derives `view_<model>` from a Django model name and populates `authStore.pagePermissions`
6
+ * - `string[]` — raw Django permission codenames (e.g. `['view_article', 'change_article']`)
7
+ */
8
+ permissions?: { model: string } | string[];
9
+ /** Required role/group names. Checked by the 'roles' middleware. */
10
+ roles?: string[];
11
+ }
12
+ }
13
+
14
+ declare module "vue-router" {
15
+ interface RouteMeta {
16
+ permissions?: { model: string } | string[];
17
+ roles?: string[];
18
+ /** Route middleware names applied to this route. Read by useCanAccessRoute for sidebar visibility. */
19
+ middleware?: string | string[];
20
+ }
21
+ }
22
+
23
+ export {};
@@ -0,0 +1,85 @@
1
+ import { ref, computed } from "vue";
2
+ import {
3
+ defineNuxtPlugin,
4
+ navigateTo,
5
+ useRoute,
6
+ useRuntimeConfig,
7
+ } from "nuxt/app";
8
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
9
+ import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
10
+ import type { MapoCoreRuntimeConfig } from "../types";
11
+
12
+ /**
13
+ * Registers the global Mapo fetch client and loading state.
14
+ *
15
+ * The plugin provides:
16
+ * - `$mapoFetch`: a shared `$fetch` instance with centralized request/response hooks.
17
+ * - `$mapoFetchLoading`: a computed boolean based on a global pending-request counter.
18
+ *
19
+ * Error handling behavior:
20
+ * - `401` (except logout calls): reset auth state and, on client, redirect to login
21
+ * with the current route encoded in a `redirect` query parameter.
22
+ * - `403`: show a global "Permission denied" error snackbar.
23
+ */
24
+ export default defineNuxtPlugin({
25
+ name: "mapo-core:fetch",
26
+ enforce: "pre",
27
+ setup() {
28
+ const rc = useRuntimeConfig().public.mapoCore as MapoCoreRuntimeConfig;
29
+ const logoutUrl = rc.logoutUrl;
30
+ const loginUrl = rc.loginUrl;
31
+
32
+ // Track whether a 401 redirect is already in flight to prevent duplicate redirects.
33
+ let redirectInFlight = false;
34
+
35
+ const handle = (status: number, requestUrl: string) => {
36
+ if (
37
+ status === 401 &&
38
+ !requestUrl.includes(logoutUrl) &&
39
+ !redirectInFlight
40
+ ) {
41
+ redirectInFlight = true;
42
+ const auth = useAuthStore();
43
+ auth.reset();
44
+ if (import.meta.client) {
45
+ const route = useRoute();
46
+ navigateTo(
47
+ `${loginUrl}?redirect=${encodeURIComponent(route.fullPath)}`,
48
+ );
49
+ }
50
+ }
51
+ if (status === 403) {
52
+ useSnackStore().show("Permission denied", "error");
53
+ }
54
+ };
55
+
56
+ // Global loading counter — incremented on every request, decremented once on every
57
+ // response (success or error) or request error. Exposed as $mapoFetchLoading so any
58
+ // component can show a spinner without wrapping every fetch call manually.
59
+ const pending = ref(0);
60
+ const mapoFetchLoading = computed(() => pending.value > 0);
61
+
62
+ const mapoFetch = $fetch.create({
63
+ onRequest() {
64
+ pending.value++;
65
+ },
66
+ onResponse({ response, request }) {
67
+ pending.value--;
68
+ handle(response.status, String(request));
69
+ },
70
+ onResponseError({ response, request, error }) {
71
+ pending.value--;
72
+ handle(response?.status ?? 0, String(request));
73
+ return Promise.reject(error);
74
+ },
75
+ onRequestError() {
76
+ // Decrement counter on network errors (no response received).
77
+ pending.value--;
78
+ },
79
+ });
80
+
81
+ return {
82
+ provide: { mapoFetch, mapoFetchLoading },
83
+ };
84
+ },
85
+ });
@@ -0,0 +1,62 @@
1
+ import {
2
+ defineNuxtPlugin,
3
+ useRuntimeConfig,
4
+ useCookie,
5
+ useRequestHeaders,
6
+ useRequestURL,
7
+ } from "nuxt/app";
8
+ import { setActivePinia } from "pinia";
9
+ import type { Pinia } from "pinia";
10
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
11
+ import { useSidebarStore } from "@mapomodule/store/runtime/stores/sidebar";
12
+ import type { MapoUser } from "@mapomodule/store/runtime/types";
13
+ import { CoreCookieEnum } from "../types";
14
+ import type { MapoCoreRuntimeConfig } from "../types";
15
+
16
+ /**
17
+ * Performs server-side bootstrap of Mapo runtime stores.
18
+ *
19
+ * On each SSR request, this plugin:
20
+ * - Activates the request Pinia instance and initializes auth/sidebar stores.
21
+ * - Restores sidebar UI state from cookies.
22
+ * - If the HttpOnly session cookie is present, calls the configured `userInfoApi`
23
+ * (forwarding request cookies) to restore the authenticated user in the auth store.
24
+ *
25
+ * On session restore failure, it resets auth state and clears the session cookie.
26
+ */
27
+ export default defineNuxtPlugin({
28
+ name: "mapo-core:init",
29
+ async setup(nuxtApp) {
30
+ const runtimeConfig = useRuntimeConfig().public.mapoCore as
31
+ | MapoCoreRuntimeConfig
32
+ | undefined;
33
+ const userInfoApi = runtimeConfig?.userInfoApi ?? "/api/profiles/me/";
34
+
35
+ const pinia = nuxtApp.$pinia as Pinia;
36
+ setActivePinia(pinia);
37
+ const authStore = useAuthStore(pinia);
38
+ const sidebarStore = useSidebarStore(pinia);
39
+
40
+ sidebarStore.hydrateFromCookies();
41
+
42
+ // The session cookie is HttpOnly — readable on SSR but not on the client.
43
+ // If present, hydrate the auth store by fetching the user with the
44
+ // incoming Cookie header forwarded to the proxy middleware.
45
+ const sessionCookie = useCookie(CoreCookieEnum.Session);
46
+ if (!sessionCookie.value) return;
47
+
48
+ try {
49
+ const headers = useRequestHeaders(["cookie"]);
50
+ const url = useRequestURL();
51
+ const absolute = `${url.protocol}//${url.host}${userInfoApi}`;
52
+ const user = await (
53
+ nuxtApp.$mapoFetch as (url: string, opts: object) => Promise<unknown>
54
+ )(absolute, { method: "GET", headers });
55
+ authStore.setUser(user as MapoUser);
56
+ } catch (e) {
57
+ console.error(`[mapo] Cannot restore session from cookie:\n${e}`);
58
+ authStore.reset();
59
+ sessionCookie.value = null;
60
+ }
61
+ },
62
+ });
@@ -0,0 +1,18 @@
1
+ import type { $Fetch } from "ofetch";
2
+
3
+ declare module "nuxt/app" {
4
+ interface NuxtApp {
5
+ $mapoFetch: $Fetch;
6
+ }
7
+ }
8
+
9
+ declare module "vue-router" {
10
+ interface RouteMeta {
11
+ permissions?: { model: string } | string[];
12
+ roles?: string[];
13
+ /** Route middleware names applied to this route. Read by useCanAccessRoute for sidebar visibility. */
14
+ middleware?: string | string[];
15
+ }
16
+ }
17
+
18
+ export {};
@@ -0,0 +1,88 @@
1
+ export enum MultipartPolicyEnum {
2
+ Auto = "auto",
3
+ Force = "force",
4
+ Disable = "disable",
5
+ }
6
+
7
+ export type MultipartPolicy = `${MultipartPolicyEnum}`;
8
+
9
+ export enum CoreCookieEnum {
10
+ Session = "__mapo_session",
11
+ }
12
+
13
+ export interface ListParams {
14
+ page?: number;
15
+ limit?: number;
16
+ search?: string;
17
+ ordering?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ export interface PaginatedResponse<T> {
22
+ results: T[];
23
+ count: number;
24
+ next?: string | null;
25
+ previous?: string | null;
26
+ }
27
+
28
+ export type ApiResponse<T> = T;
29
+
30
+ export interface CrudConfig {
31
+ baseURL?: string;
32
+ headers?: Record<string, string>;
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ export interface CrudOptions {
37
+ multipart?: MultipartPolicy;
38
+ }
39
+
40
+ export interface CrudRepository<T> {
41
+ list(params?: ListParams, config?: CrudConfig): Promise<PaginatedResponse<T>>;
42
+ detail(id: string | number, config?: CrudConfig): Promise<T>;
43
+ create(data: Partial<T>, config?: CrudConfig, opts?: CrudOptions): Promise<T>;
44
+ update(
45
+ id: string | number,
46
+ data: Partial<T>,
47
+ config?: CrudConfig,
48
+ opts?: CrudOptions,
49
+ ): Promise<T>;
50
+ partialUpdate(
51
+ id: string | number,
52
+ diff: Partial<T>,
53
+ config?: CrudConfig,
54
+ opts?: CrudOptions,
55
+ ): Promise<T>;
56
+ delete(id: string | number, config?: CrudConfig): Promise<void>;
57
+ updateOrCreate(
58
+ data: Partial<T> & { id?: string | number },
59
+ config?: CrudConfig,
60
+ opts?: CrudOptions,
61
+ ): Promise<T>;
62
+ updateOrder(
63
+ startId: string | number,
64
+ endId: string | number,
65
+ config?: CrudConfig,
66
+ ): Promise<void>;
67
+ }
68
+
69
+ export interface Credentials {
70
+ username: string;
71
+ password: string;
72
+ }
73
+
74
+ export interface AuthToken {
75
+ token: string;
76
+ [key: string]: unknown;
77
+ }
78
+
79
+ /**
80
+ * Shape of `runtimeConfig.public.mapoCore` as written by the module setup.
81
+ * All entries are guaranteed by the module `defaults`.
82
+ */
83
+ export interface MapoCoreRuntimeConfig {
84
+ authLoginUrl: string;
85
+ userInfoApi: string;
86
+ logoutUrl: string;
87
+ loginUrl: string;
88
+ }
@@ -0,0 +1,41 @@
1
+ import { useNuxtApp } from "nuxt/app";
2
+ import type { $Fetch } from "ofetch";
3
+ import type { ComputedRef } from "vue";
4
+
5
+ export interface UseMapoFetchReturn {
6
+ /** The auth-aware `$mapoFetch` instance (same one provided by the plugin). */
7
+ fetch: $Fetch;
8
+ /**
9
+ * `true` while at least one `$mapoFetch` request is in flight — globally,
10
+ * across the entire app. Incremented by the plugin-level `onRequest` hook
11
+ * and decremented by `onResponse` / `onResponseError`, so it stays accurate
12
+ * even for raw `useNuxtApp().$mapoFetch` calls outside this composable.
13
+ */
14
+ loading: ComputedRef<boolean>;
15
+ }
16
+
17
+ /**
18
+ * Returns the global `$mapoFetch` instance together with a centralized
19
+ * `loading` indicator that tracks **all** in-flight `$mapoFetch` requests,
20
+ * not just the ones made through this composable.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const { fetch, loading } = useMapoFetch()
25
+ * await fetch('/api/articles/', { method: 'POST', body: payload })
26
+ * ```
27
+ *
28
+ * ```vue
29
+ * <UButton :loading="loading" @click="submit">Save</UButton>
30
+ * ```
31
+ */
32
+ export function useMapoFetch(): UseMapoFetchReturn {
33
+ const nuxt = useNuxtApp() as unknown as {
34
+ $mapoFetch: $Fetch;
35
+ $mapoFetchLoading: ComputedRef<boolean>;
36
+ };
37
+ return {
38
+ fetch: nuxt.$mapoFetch,
39
+ loading: nuxt.$mapoFetchLoading,
40
+ };
41
+ }
package/src/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface MapoOptions {
2
+ authLoginUrl?: string;
3
+ userInfoApi?: string;
4
+ logoutUrl?: string;
5
+ loginUrl?: string;
6
+ }
7
+
8
+ export const MAPO_DEFAULTS = {
9
+ authLoginUrl: "/api/auth/login",
10
+ userInfoApi: "/api/profiles/me/",
11
+ logoutUrl: "/api/auth/logout",
12
+ loginUrl: "/login",
13
+ } satisfies Required<MapoOptions>;