@visin/frontend-core 0.1.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.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @visin/frontend-core
2
+
3
+ Shared frontend auth/API-client logic for Visin frontends.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @visin/frontend-core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createApiClient } from '@visin/frontend-core';
15
+
16
+ export const api = createApiClient({
17
+ baseUrl: () => getGlobalConfig().VISION_API_URL,
18
+ getToken: () => localStorage.getItem('authToken'),
19
+ onUnauthorized: () => { window.location.href = '/login'; }
20
+ });
21
+
22
+ const { id } = await api.get('/projects/123');
23
+ ```
24
+
25
+ ## Status
26
+
27
+ Only `createApiClient` is extracted so far — it matches the Bearer-token-from-
28
+ `localStorage` transport already used independently in `account-front` and
29
+ `vision-front`. Auth state (login/logout/session) is **not** unified yet:
30
+ `account-front`, `auth-front`, and `vision-front` each implement it differently
31
+ (`authService.ts`, `authFlow.ts`, `AuthContext.tsx`) rather than as copies of one
32
+ pattern, so that needs its own design pass before landing here. See the repo's
33
+ `TODO.md`.
@@ -0,0 +1,37 @@
1
+ export interface ApiClientOptions {
2
+ /** Read fresh at call time (app config may not be resolved yet at module load). */
3
+ baseUrl: () => string;
4
+ /** Reads the current auth token, e.g. `() => localStorage.getItem('authToken')`. */
5
+ getToken?: () => string | null;
6
+ /** Called on a 401 response, e.g. to redirect to the login page. */
7
+ onUnauthorized?: () => void;
8
+ }
9
+ export interface ApiRequestOptions extends RequestInit {
10
+ /** Skip the onUnauthorized callback for this call (e.g. an auth-check that expects 401 as a normal "not logged in" result, not a hard redirect). */
11
+ skipAuthRedirect?: boolean;
12
+ }
13
+ export declare class ApiError extends Error {
14
+ status: number;
15
+ body: unknown;
16
+ constructor(status: number, message: string, body?: unknown);
17
+ }
18
+ /**
19
+ * Generic authenticated JSON fetch wrapper — base URL handling, Bearer-token
20
+ * auth, structured errors, and an opt-in 401 → redirect-to-auth hook.
21
+ *
22
+ * Matches the auth transport all four Visin services actually use today
23
+ * (a Bearer token read from `localStorage`, not a cookie), unlike a
24
+ * cookie-based client would. Each front currently hand-rolls this
25
+ * (`account-front/services/authService.ts`, `vision-front/config/visionApi.ts`)
26
+ * with near-identical logic; new call sites can adopt this instead.
27
+ */
28
+ export declare function createApiClient(options: ApiClientOptions): {
29
+ request: <T = unknown>(path: string, init?: ApiRequestOptions) => Promise<T>;
30
+ get: <T = unknown>(path: string, init?: ApiRequestOptions) => Promise<T>;
31
+ post: <T = unknown>(path: string, body?: unknown, init?: ApiRequestOptions) => Promise<T>;
32
+ put: <T = unknown>(path: string, body?: unknown, init?: ApiRequestOptions) => Promise<T>;
33
+ patch: <T = unknown>(path: string, body?: unknown, init?: ApiRequestOptions) => Promise<T>;
34
+ delete: <T = unknown>(path: string, init?: ApiRequestOptions) => Promise<T>;
35
+ };
36
+ export type ApiClient = ReturnType<typeof createApiClient>;
37
+ //# sourceMappingURL=apiClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiClient.d.ts","sourceRoot":"","sources":["../src/apiClient.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,oJAAoJ;IACpJ,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;gBAEF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;CAM5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB;cAChC,CAAC,kBAAkB,MAAM,SAAQ,iBAAiB,KAAQ,OAAO,CAAC,CAAC,CAAC;UAoCnF,CAAC,kBAAkB,MAAM,SAAS,iBAAiB;WAClD,CAAC,kBAAkB,MAAM,SAAS,OAAO,SAAS,iBAAiB;UAEpE,CAAC,kBAAkB,MAAM,SAAS,OAAO,SAAS,iBAAiB;YAEjE,CAAC,kBAAkB,MAAM,SAAS,OAAO,SAAS,iBAAiB;aAElE,CAAC,kBAAkB,MAAM,SAAS,iBAAiB;EAE/D;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC"}
@@ -0,0 +1,59 @@
1
+ export class ApiError extends Error {
2
+ constructor(status, message, body) {
3
+ super(message);
4
+ this.name = 'ApiError';
5
+ this.status = status;
6
+ this.body = body;
7
+ }
8
+ }
9
+ /**
10
+ * Generic authenticated JSON fetch wrapper — base URL handling, Bearer-token
11
+ * auth, structured errors, and an opt-in 401 → redirect-to-auth hook.
12
+ *
13
+ * Matches the auth transport all four Visin services actually use today
14
+ * (a Bearer token read from `localStorage`, not a cookie), unlike a
15
+ * cookie-based client would. Each front currently hand-rolls this
16
+ * (`account-front/services/authService.ts`, `vision-front/config/visionApi.ts`)
17
+ * with near-identical logic; new call sites can adopt this instead.
18
+ */
19
+ export function createApiClient(options) {
20
+ async function request(path, init = {}) {
21
+ const { skipAuthRedirect, headers, ...rest } = init;
22
+ const token = options.getToken?.();
23
+ const response = await fetch(`${options.baseUrl()}${path}`, {
24
+ ...rest,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ ...(token && { Authorization: `Bearer ${token}` }),
28
+ ...headers
29
+ }
30
+ });
31
+ if (response.status === 401 && !skipAuthRedirect) {
32
+ options.onUnauthorized?.();
33
+ }
34
+ if (!response.ok) {
35
+ let body;
36
+ try {
37
+ body = await response.json();
38
+ }
39
+ catch {
40
+ // response wasn't JSON — leave body undefined
41
+ }
42
+ const message = body?.message ?? `HTTP error! status: ${response.status}`;
43
+ throw new ApiError(response.status, message, body);
44
+ }
45
+ if (response.status === 204) {
46
+ return undefined;
47
+ }
48
+ return response.json();
49
+ }
50
+ return {
51
+ request,
52
+ get: (path, init) => request(path, { ...init, method: 'GET' }),
53
+ post: (path, body, init) => request(path, { ...init, method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }),
54
+ put: (path, body, init) => request(path, { ...init, method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }),
55
+ patch: (path, body, init) => request(path, { ...init, method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }),
56
+ delete: (path, init) => request(path, { ...init, method: 'DELETE' })
57
+ };
58
+ }
59
+ //# sourceMappingURL=apiClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiClient.js","sourceRoot":"","sources":["../src/apiClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { createApiClient, ApiError } from './apiClient';
2
+ export type { ApiClientOptions, ApiRequestOptions, ApiClient } from './apiClient';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // @visin/frontend-core - Shared frontend auth/API-client logic for Visin frontends
2
+ export { createApiClient, ApiError } from './apiClient';
3
+ // Not yet extracted: account-front's `authService.ts`, auth-front's
4
+ // `authFlow.ts`, and vision-front's `AuthContext.tsx` implement auth
5
+ // (token storage, login/logout, session checks) three different ways
6
+ // rather than as copies of one pattern, so unifying them needs its own
7
+ // design pass across all three before landing here — see TODO.md.
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGxD,oEAAoE;AACpE,qEAAqE;AACrE,qEAAqE;AACrE,uEAAuE;AACvE,kEAAkE"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@visin/frontend-core",
3
+ "version": "0.1.1",
4
+ "description": "Shared frontend auth/API-client logic for Visin frontends",
5
+ "author": "Toomas Tahves",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/visin-platform/visin-monorepo.git",
16
+ "directory": "libs/frontend-core"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "clean": "rm -rf dist",
21
+ "prepublishOnly": "npm run clean && npm run build",
22
+ "release": "standard-version && npm run build && npm publish --access public",
23
+ "release:beta": "standard-version --prerelease beta && npm run build && npm publish --access public --tag beta",
24
+ "test": "vitest run",
25
+ "test:coverage": "vitest run --coverage",
26
+ "lint": "eslint . --max-warnings 0",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "keywords": [
30
+ "visin",
31
+ "frontend",
32
+ "shared",
33
+ "auth",
34
+ "api-client"
35
+ ],
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "@types/node": "^26.0.1",
39
+ "@vitest/coverage-v8": "^3.2.4",
40
+ "eslint": "^10.6.0",
41
+ "jsdom": "^26.1.0",
42
+ "standard-version": "^9.5.0",
43
+ "typescript": "^6.0.3",
44
+ "typescript-eslint": "^8.62.1",
45
+ "vitest": "^3.2.4"
46
+ }
47
+ }