@nexys/user-management-sdk 0.0.2

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,3 @@
1
+ # User Management SDK
2
+
3
+ [![Test Package](https://github.com/nexys-system/user-management-client/actions/workflows/test.yml/badge.svg)](https://github.com/nexys-system/user-management-client/actions/workflows/test.yml)
@@ -0,0 +1,29 @@
1
+ import { Locale, Profile, SSOService, UserAdmin, Permission } from "./type";
2
+ declare class AuthClient {
3
+ apiBasename: string;
4
+ constructor(apiBasename: string);
5
+ authSSOUrl: (service: SSOService) => Promise<{
6
+ url: string;
7
+ }>;
8
+ authGoogleRedirect: (ssoService: SSOService, code: string, state: string) => Promise<{
9
+ profile: {
10
+ id: string;
11
+ };
12
+ permissions: number[];
13
+ locale: {
14
+ country: string;
15
+ lang: string;
16
+ };
17
+ }>;
18
+ getProfile: () => Promise<{
19
+ profile: Profile;
20
+ permissions: Permission[];
21
+ locale: Locale;
22
+ }>;
23
+ authLogout: () => Promise<any>;
24
+ adminUserList: () => Promise<UserAdmin[]>;
25
+ authRefresh: () => Promise<{
26
+ message: string;
27
+ }>;
28
+ }
29
+ export default AuthClient;
package/dist/client.js ADDED
@@ -0,0 +1,49 @@
1
+ // auth stuff
2
+ const redirectUrl = (ssoService) => [window.location.origin, "auth", "sso", ssoService, "redirect"].join("/");
3
+ class AuthClient {
4
+ apiBasename;
5
+ constructor(apiBasename) {
6
+ this.apiBasename = apiBasename;
7
+ }
8
+ authSSOUrl = async (service) => {
9
+ const response = await fetch(`${this.apiBasename}/auth/${service}/url?redirectUrl=${encodeURIComponent(redirectUrl(service))}`);
10
+ return response.json();
11
+ };
12
+ authGoogleRedirect = async (ssoService, code, state) => {
13
+ const response = await fetch(this.apiBasename +
14
+ "/auth/" +
15
+ ssoService +
16
+ "/redirect?code=" +
17
+ encodeURIComponent(code) +
18
+ "&state=" +
19
+ encodeURIComponent(state));
20
+ const j = await response.json();
21
+ if (response.ok) {
22
+ return j;
23
+ }
24
+ return j;
25
+ };
26
+ getProfile = async () => {
27
+ const response = await fetch(this.apiBasename + "/profile");
28
+ const j = await response.json();
29
+ if (!response.ok) {
30
+ throw j;
31
+ }
32
+ return j;
33
+ };
34
+ authLogout = async () => {
35
+ const response = await fetch(this.apiBasename + "/auth/logout");
36
+ return response.json();
37
+ };
38
+ adminUserList = async () => {
39
+ const response = await fetch(this.apiBasename + "/admin/user/list", {
40
+ method: "POST",
41
+ });
42
+ return response.json();
43
+ };
44
+ authRefresh = async () => {
45
+ const response = await fetch(this.apiBasename + "/auth/refresh");
46
+ return response.json();
47
+ };
48
+ }
49
+ export default AuthClient;
@@ -0,0 +1,16 @@
1
+ import React, { ReactNode } from "react";
2
+ import AuthClient from "./client.js";
3
+ import { Profile } from "./type.js";
4
+ export type AuthContextType = {
5
+ profile: Profile;
6
+ logout: () => void;
7
+ };
8
+ export declare const AdminContext: React.Context<AuthContextType | undefined>;
9
+ export declare const AuthProvider: ({ authClient, Spinner, loginPath, }: {
10
+ authClient: AuthClient;
11
+ Spinner: () => JSX.Element;
12
+ loginPath: string;
13
+ }) => ({ children }: {
14
+ children: ReactNode;
15
+ }) => import("react/jsx-runtime").JSX.Element;
16
+ export declare const useAuth: () => AuthContextType;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { createContext, useContext, useState } from "react";
3
+ import { useNavigate } from "react-router-dom";
4
+ export const AdminContext = createContext(undefined);
5
+ export const AuthProvider = ({ authClient, Spinner, loginPath, }) => ({ children }) => {
6
+ const navigate = useNavigate();
7
+ const [profile, setProfile] = useState(null);
8
+ React.useEffect(() => {
9
+ try {
10
+ authClient
11
+ .getProfile()
12
+ .then(({ profile }) => setProfile(profile))
13
+ .catch(async () => {
14
+ try {
15
+ await authClient.authRefresh();
16
+ const { profile } = await authClient.getProfile();
17
+ setProfile(profile);
18
+ }
19
+ catch (e) {
20
+ console.log("e1", e);
21
+ const navigationMessage = {
22
+ message: "Your session is no longer active, you were redirected",
23
+ contextClass: "warning",
24
+ };
25
+ navigate("/login", { state: navigationMessage });
26
+ }
27
+ });
28
+ }
29
+ catch (err) {
30
+ console.log("err2", err);
31
+ const navigationMessage = {
32
+ message: "Your session is no longer active, you were redirected",
33
+ contextClass: "warning",
34
+ };
35
+ return navigate(loginPath, { state: navigationMessage });
36
+ }
37
+ }, []);
38
+ if (profile === null) {
39
+ return _jsx(Spinner, {});
40
+ }
41
+ const logout = () => {
42
+ setProfile(null);
43
+ authClient.authLogout();
44
+ const navigationMessage = {
45
+ message: "You were successfully logged out",
46
+ contextClass: "info",
47
+ };
48
+ const q = btoa([profile.id, profile.instance.uuid].join(":"));
49
+ return navigate(loginPath + "?q=" + q, {
50
+ state: navigationMessage,
51
+ });
52
+ };
53
+ const value = {
54
+ profile,
55
+ logout,
56
+ };
57
+ return (_jsx(AdminContext.Provider, { value: value, children: children }));
58
+ };
59
+ export const useAuth = () => {
60
+ const context = useContext(AdminContext);
61
+ if (context === undefined) {
62
+ throw new Error("useAdminUseCase must be used within an AuthProvider");
63
+ }
64
+ return context;
65
+ };
@@ -0,0 +1,4 @@
1
+ export declare const getLoginInfoFromQuery: (lSearch: string) => {
2
+ userId: string;
3
+ instanceId: string;
4
+ } | null;
@@ -0,0 +1,9 @@
1
+ export const getLoginInfoFromQuery = (lSearch) => {
2
+ const queryParams = new URLSearchParams(lSearch);
3
+ const paramValue = queryParams.get("q");
4
+ if (!paramValue) {
5
+ return null;
6
+ }
7
+ const [userId, instanceId] = atob(paramValue).split(":");
8
+ return { userId, instanceId };
9
+ };
@@ -0,0 +1,13 @@
1
+ import AuthClient from "./client.js";
2
+ declare const SsoResponse: ({ authClient, links, Banner, Spinner, }: {
3
+ authClient: AuthClient;
4
+ links: {
5
+ home: string;
6
+ login: string;
7
+ };
8
+ Banner: (props: {
9
+ label: string;
10
+ }) => JSX.Element;
11
+ Spinner: () => JSX.Element;
12
+ }) => () => JSX.Element;
13
+ export default SsoResponse;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigate, useParams } from "react-router-dom";
3
+ const SsoResponse = ({ authClient, links, Banner, Spinner, }) => () => {
4
+ const navigate = useNavigate();
5
+ const params = useParams();
6
+ const queryParams = new URLSearchParams(window.location.search);
7
+ const code = queryParams.get("code");
8
+ const state = queryParams.get("state");
9
+ const ssoService = params.ssoService;
10
+ if (!ssoService) {
11
+ return _jsx(Banner, { label: "Could not find associated sso service" });
12
+ }
13
+ if (code === null) {
14
+ return _jsx(Banner, { label: "Code must be given" });
15
+ }
16
+ if (state === null) {
17
+ return _jsx(Banner, { label: "State must be given" });
18
+ }
19
+ authClient
20
+ .authGoogleRedirect(ssoService, code, state)
21
+ .then(() => {
22
+ navigate(links.home);
23
+ })
24
+ .catch((err) => {
25
+ console.log("err", err);
26
+ const navigationMessage = {
27
+ message: "Could not log you in",
28
+ contextClass: "error",
29
+ };
30
+ navigate(links.login, {
31
+ state: navigationMessage,
32
+ });
33
+ });
34
+ return _jsx(Spinner, {});
35
+ };
36
+ export default SsoResponse;
package/dist/type.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface Profile {
2
+ id: string;
3
+ firstName: string;
4
+ lastName: string;
5
+ email: string;
6
+ instance: {
7
+ uuid: string;
8
+ name: string;
9
+ };
10
+ }
11
+ export interface Locale {
12
+ country: string;
13
+ lang: string;
14
+ }
15
+ export declare enum UserStatus {
16
+ active = 1,
17
+ inactive = 2,
18
+ pending = 3
19
+ }
20
+ export interface UserAdmin extends Omit<Profile, "id"> {
21
+ uuid: string;
22
+ locale: Locale;
23
+ status: UserStatus;
24
+ }
25
+ export type SSOService = "microsoft" | "apple" | "github" | "google";
26
+ export declare const ssoAvailable: SSOService[];
27
+ export type ContextClass = "success" | "error" | "warning" | "info" | "default";
28
+ export interface NavigationMessage {
29
+ message: string;
30
+ contextClass: ContextClass;
31
+ }
32
+ export declare enum Permission {
33
+ app = 1,
34
+ admin = 2,
35
+ superadmin = 3
36
+ }
package/dist/type.js ADDED
@@ -0,0 +1,12 @@
1
+ export var UserStatus;
2
+ (function (UserStatus) {
3
+ UserStatus[UserStatus["active"] = 1] = "active";
4
+ UserStatus[UserStatus["inactive"] = 2] = "inactive";
5
+ UserStatus[UserStatus["pending"] = 3] = "pending";
6
+ })(UserStatus || (UserStatus = {}));
7
+ export const ssoAvailable = [
8
+ "microsoft",
9
+ "apple",
10
+ "github",
11
+ "google",
12
+ ];
@@ -0,0 +1,6 @@
1
+ export declare const getLoginInfoFromQuery: (lSearch: string) => {
2
+ userId: string;
3
+ instanceId: string;
4
+ } | null;
5
+ export declare const base64UrlToUint8Array: (base64Url: string) => Uint8Array;
6
+ export declare const arrayBufferToBase64: (buffer: ArrayBuffer) => string;
package/dist/utils.js ADDED
@@ -0,0 +1,34 @@
1
+ export const getLoginInfoFromQuery = (lSearch) => {
2
+ const queryParams = new URLSearchParams(lSearch);
3
+ const paramValue = queryParams.get("q");
4
+ if (!paramValue) {
5
+ return null;
6
+ }
7
+ const [userId, instanceId] = atob(paramValue).split(":");
8
+ return { userId, instanceId };
9
+ };
10
+ export const base64UrlToUint8Array = (base64Url) => {
11
+ // Replace Base64URL characters with Base64 characters
12
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
13
+ // Decode Base64 string to binary string
14
+ const binaryString = atob(base64);
15
+ // Create a Uint8Array from the binary string
16
+ const bytes = new Uint8Array(binaryString.length);
17
+ for (let i = 0; i < binaryString.length; i++) {
18
+ bytes[i] = binaryString.charCodeAt(i);
19
+ }
20
+ return bytes;
21
+ };
22
+ export const arrayBufferToBase64 = (buffer) => {
23
+ let binary = "";
24
+ const bytes = new Uint8Array(buffer);
25
+ const len = bytes.byteLength;
26
+ for (let i = 0; i < len; i++) {
27
+ binary += String.fromCharCode(bytes[i]);
28
+ }
29
+ // Convert binary data to a base64 string
30
+ const base64 = btoa(binary);
31
+ // Optionally, you might want to make the base64 string URL-safe
32
+ // by replacing "+" with "-", "/" with "_", and stripping "=" padding
33
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ // Import necessary modules
2
+ import { strict as assert } from "node:assert";
3
+ import { describe, it } from "node:test";
4
+ import { getLoginInfoFromQuery, base64UrlToUint8Array, arrayBufferToBase64, } from "./utils.js";
5
+ // Mock browser's atob and btoa for Node.js
6
+ global.atob = (base64) => Buffer.from(base64, "base64").toString("binary");
7
+ global.btoa = (binary) => Buffer.from(binary, "binary").toString("base64");
8
+ // Describe the test suite
9
+ describe("getLoginInfoFromQuery", () => {
10
+ it('should return null if the query does not contain the "q" parameter', () => {
11
+ const result = getLoginInfoFromQuery("?someOtherParam=abc");
12
+ assert.strictEqual(result, null);
13
+ });
14
+ it('should return null if the "q" parameter is missing', () => {
15
+ const result = getLoginInfoFromQuery("");
16
+ assert.strictEqual(result, null);
17
+ });
18
+ it('should decode the "q" parameter and return the correct userId and instanceId', () => {
19
+ const encodedParam = Buffer.from("user123:instance456").toString("base64");
20
+ const query = `?q=${encodedParam}`;
21
+ const result = getLoginInfoFromQuery(query);
22
+ assert.deepStrictEqual(result, {
23
+ userId: "user123",
24
+ instanceId: "instance456",
25
+ });
26
+ });
27
+ it('should handle missing ":" in decoded string and return undefined values', () => {
28
+ const encodedParam = Buffer.from("user123").toString("base64");
29
+ const query = `?q=${encodedParam}`;
30
+ const result = getLoginInfoFromQuery(query);
31
+ assert.deepStrictEqual(result, {
32
+ userId: "user123",
33
+ instanceId: undefined,
34
+ });
35
+ });
36
+ });
37
+ // Tests for base64UrlToUint8Array
38
+ describe("base64UrlToUint8Array", () => {
39
+ it("should correctly convert Base64URL string to Uint8Array", () => {
40
+ const base64Url = "SGVsbG8td29ybGQ_"; // Base64URL for "Hello-world?"
41
+ const expectedArray = new Uint8Array([
42
+ 72, 101, 108, 108, 111, 45, 119, 111, 114, 108, 100, 63,
43
+ ]);
44
+ const result = base64UrlToUint8Array(base64Url);
45
+ assert.deepStrictEqual(result, expectedArray);
46
+ });
47
+ it("should handle padding correctly", () => {
48
+ const base64Url = "SGVsbG8gd29ybGQ"; // Base64URL for "Hello world" without padding
49
+ const expectedArray = new Uint8Array([
50
+ 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
51
+ ]);
52
+ const result = base64UrlToUint8Array(base64Url);
53
+ assert.deepStrictEqual(result, expectedArray);
54
+ });
55
+ });
56
+ // Tests for arrayBufferToBase64
57
+ describe("arrayBufferToBase64", () => {
58
+ it("should correctly convert ArrayBuffer to Base64URL string", () => {
59
+ const buffer = new Uint8Array([
60
+ 72, 101, 108, 108, 111, 45, 119, 111, 114, 108, 100, 63,
61
+ ]).buffer;
62
+ const expectedBase64Url = "SGVsbG8td29ybGQ_";
63
+ const result = arrayBufferToBase64(buffer);
64
+ assert.strictEqual(result, expectedBase64Url);
65
+ });
66
+ it("should handle empty ArrayBuffer", () => {
67
+ const buffer = new ArrayBuffer(0);
68
+ const expectedBase64Url = "";
69
+ const result = arrayBufferToBase64(buffer);
70
+ assert.strictEqual(result, expectedBase64Url);
71
+ });
72
+ it("should handle large ArrayBuffer", () => {
73
+ const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).buffer;
74
+ const expectedBase64Url = "AAECAwQFBgcICQ";
75
+ const result = arrayBufferToBase64(buffer);
76
+ assert.strictEqual(result, expectedBase64Url);
77
+ });
78
+ });
@@ -0,0 +1,9 @@
1
+ export declare const login: (user: {
2
+ uuid: string;
3
+ }, instance: {
4
+ uuid: string;
5
+ }) => Promise<any>;
6
+ export declare const register: () => Promise<{
7
+ isValidSignature: boolean;
8
+ uuid: string;
9
+ }>;
@@ -0,0 +1,150 @@
1
+ import * as U from "./utils";
2
+ // webauthn client
3
+ const getPreLogin = async (user, instance) => {
4
+ const r = await fetch("/napi/auth/webauthn/prelogin", {
5
+ method: "POST",
6
+ headers: { "content-type": "application/json" },
7
+ body: JSON.stringify({ uuid: user.uuid, instance }), // send user id
8
+ });
9
+ const j = await r.json();
10
+ if (!r.ok && "error" in j) {
11
+ throw Error(j.error);
12
+ }
13
+ return j;
14
+ };
15
+ const loginRequest = async (jwt, { credentialId, authenticatorData, clientDataJSON, signature, }) => {
16
+ const respose = await fetch("/napi/auth/webauthn/login", {
17
+ method: "POST",
18
+ body: JSON.stringify({
19
+ jwt,
20
+ credentialId,
21
+ authenticatorData,
22
+ clientDataJSON,
23
+ signature,
24
+ }),
25
+ headers: { "content-type": "application/json" },
26
+ });
27
+ return respose.json();
28
+ };
29
+ function uuidToUint8Array(uuid) {
30
+ // Remove all hyphens from the UUID
31
+ const hexNoDashes = uuid.replace(/-/g, "");
32
+ // Make sure the cleaned hex string is of length 32
33
+ if (hexNoDashes.length !== 32) {
34
+ throw new Error("Invalid UUID format");
35
+ }
36
+ const arrayBuffer = new Uint8Array(16);
37
+ for (let i = 0, j = 0; i < 32; i += 2, j++) {
38
+ // Convert each pair of hexadecimal characters to a byte
39
+ arrayBuffer[j] = parseInt(hexNoDashes.substr(i, 2), 16);
40
+ }
41
+ return arrayBuffer;
42
+ }
43
+ export const login = async (user, instance) => {
44
+ const { challenge, authenticators } = await getPreLogin(user, instance);
45
+ // console.log({ challenge });
46
+ const transports = [
47
+ // "usb",
48
+ // "nfc",
49
+ // "ble",
50
+ "internal",
51
+ ]; // Specify acceptable transports if known
52
+ const allowCredentials = authenticators.map((cred) => ({
53
+ id: U.base64UrlToUint8Array(cred.credentialId), // Convert from Base64 URL to Uint8Array
54
+ type: "public-key",
55
+ transports,
56
+ }));
57
+ const newCredentialInfo = await navigator.credentials.get({
58
+ publicKey: {
59
+ challenge: U.base64UrlToUint8Array(challenge),
60
+ allowCredentials,
61
+ userVerification: "required", // "preferred", // or 'required' or 'discouraged'
62
+ timeout: 60000, // Optional: adjust according to your needs
63
+ },
64
+ });
65
+ if (!newCredentialInfo) {
66
+ throw Error("failing to retrieve credentials");
67
+ }
68
+ // Find which credential was used
69
+ const matchedCredential = authenticators.find((cred) => U.base64UrlToUint8Array(cred.credentialId).toString() ===
70
+ new Uint8Array(newCredentialInfo.rawId).toString());
71
+ // console.log(matchedCredential, "!");
72
+ if (!matchedCredential) {
73
+ throw Error("could not match credentials");
74
+ }
75
+ const { response } = newCredentialInfo;
76
+ const authenticatorData = U.arrayBufferToBase64(response.authenticatorData);
77
+ const clientDataJSON = U.arrayBufferToBase64(response.clientDataJSON);
78
+ const signature = U.arrayBufferToBase64(response.signature);
79
+ return loginRequest(matchedCredential.jwt, {
80
+ credentialId: matchedCredential.credentialId,
81
+ authenticatorData,
82
+ clientDataJSON,
83
+ signature,
84
+ });
85
+ };
86
+ const preRegister = async () => {
87
+ const response = await fetch("/napi/profile/webauthn/register");
88
+ return response.json();
89
+ };
90
+ const registerRequest = async ({ credentialId, attestationObject, clientDataJSON, jwt, }) => {
91
+ const respose = await fetch("/napi/profile/webauthn/register", {
92
+ method: "POST",
93
+ body: JSON.stringify({
94
+ jwt,
95
+ credentialId,
96
+ attestationObject,
97
+ clientDataJSON,
98
+ }),
99
+ headers: { "content-type": "application/json" },
100
+ });
101
+ return respose.json();
102
+ };
103
+ export const register = async () => {
104
+ //getChallenge();
105
+ const { challenge, rp, user, jwt } = await preRegister();
106
+ const publicKey = {
107
+ // Relying Party (your service)
108
+ rp,
109
+ // User Information
110
+ user: {
111
+ id: uuidToUint8Array(user.uuid),
112
+ name: user.name,
113
+ displayName: user.displayName,
114
+ },
115
+ // Cryptographic challenge from the server
116
+ challenge: U.base64UrlToUint8Array(challenge),
117
+ // Public key parameters
118
+ pubKeyCredParams: [
119
+ { alg: -7, type: "public-key" }, // ES256
120
+ { alg: -257, type: "public-key" }, // RS256
121
+ ], // Registration timeout
122
+ timeout: 60000, // 60 seconds
123
+ // Attestation preference
124
+ attestation: "direct",
125
+ // Exclude existing credentials to prevent re-registration
126
+ excludeCredentials: [], // Use this to exclude already registered credentials
127
+ authenticatorSelection: {
128
+ authenticatorAttachment: ["platform", "cross-platform"], // for now platform, in a second step add phones and external keys
129
+ requireResidentKey: false,
130
+ userVerification: "preferred",
131
+ },
132
+ };
133
+ // Request the creation of new credentials
134
+ const newCredentialInfo = (await navigator.credentials.create({
135
+ publicKey,
136
+ }));
137
+ const credentialId = newCredentialInfo.id;
138
+ // const publicKey = new TextDecoder().decode(
139
+ // newCredentialInfo.response.clientDataJSON
140
+ //); // For demonstration; actual extraction differs
141
+ const attestationObject = U.arrayBufferToBase64(newCredentialInfo.response.attestationObject);
142
+ const clientDataJSON = U.arrayBufferToBase64(newCredentialInfo.response.clientDataJSON);
143
+ console.log({ credentialId, attestationObject, clientDataJSON });
144
+ return registerRequest({
145
+ jwt,
146
+ credentialId,
147
+ attestationObject,
148
+ clientDataJSON,
149
+ });
150
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@nexys/user-management-sdk",
3
+ "version": "0.0.2",
4
+ "private": false,
5
+ "description": "react client/sdk that faciliates connecting to the user management",
6
+ "main": "dist/index.js",
7
+ "devDependencies": {
8
+ "@types/node": "^22.5.4",
9
+ "@types/react": "^18.3.5",
10
+ "react": "^18.3.1",
11
+ "react-router-dom": "^6.26.2",
12
+ "typescript": "^5.6.2"
13
+ },
14
+ "type": "module",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "node dist/*.test.js",
21
+ "buildPackage": "yarn build; rm dist/*.test.js; rm dist/*.test.d.ts"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+ssh://git@github.com/nexys-system/user-management-client.git"
26
+ },
27
+ "author": "Nexys",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/nexys-system/user-management-client/issues"
31
+ },
32
+ "homepage": "https://github.com/nexys-system/user-management-client#readme"
33
+ }