Skip to content

Commit

Permalink
Merge pull request #4 from aversini/feat-AuthProvider-MVP
Browse files Browse the repository at this point in the history
feat: AuthProvider MVP
  • Loading branch information
aversini authored Jun 14, 2024
2 parents 5f19883 + e92037e commit 0c18c5c
Show file tree
Hide file tree
Showing 20 changed files with 10,127 additions and 8 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/pull-requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ jobs:
with:
refresh-message-position: true
message-path: |
packages/ui-components/tmp/pr-stats.md
packages/ui-form/tmp/pr-stats.md
packages/ui-system/tmp/pr-stats.md
packages/auth-provider/tmp/pr-stats.md
inspect:
if: ${{ always() }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.NODE_CLI }}
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"test:coverage": "lerna run test:coverage"
},
"devDependencies": {
"@node-cli/bundlesize": "4.0.4",
"@versini/dev-dependencies-client": "5.0.1",
"@versini/dev-dependencies-types": "1.2.1"
"@node-cli/bundlesize": "4.1.0",
"@versini/dev-dependencies-client": "5.1.0",
"@versini/dev-dependencies-types": "1.3.0"
},
"packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b"
"packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631"
}
3 changes: 3 additions & 0 deletions packages/auth-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @versini/auth-provider

This package provides a simple authentication provider for your application.
15 changes: 15 additions & 0 deletions packages/auth-provider/bundlesize.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
report: {
previous: "stats/stats.json",
current: "tmp/stats.json",
},
sizes: [
/**
* JavaScript static assets.
*/
{
path: "dist/index.js",
limit: "3 kb",
},
],
};
48 changes: 48 additions & 0 deletions packages/auth-provider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@versini/auth-provider",
"version": "0.0.0",
"license": "MIT",
"author": "Arno Versini",
"publishConfig": {
"access": "public"
},
"homepage": "https://github.com/aversini/auth-client",
"repository": {
"type": "git",
"url": "[email protected]:aversini/auth-client.git"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build:check": "tsc",
"build:js": "vite build",
"build:types": "tsup",
"build": "npm-run-all --serial clean build:check build:js build:types",
"clean": "rimraf dist tmp",
"dev:js": "vite build --watch --mode development",
"dev:types": "tsup --watch src",
"dev": "npm-run-all clean --parallel dev:js dev:types",
"lint": "biome lint src",
"start": "static-server dist --port 5173",
"stats:pr": "bundlesize -c bundlesize.config.js -p \"$npm_package_version\" -o tmp/stats.json --silent",
"stats:release": "bundlesize -c bundlesize.config.js -p \"$npm_package_version\" -o stats/stats.json --silent",
"stats:report": "bundlesize -c bundlesize.config.js --type report -o tmp/pr-stats.md --silent",
"stats": "bundlesize -c bundlesize.config.js -p \"$npm_package_version\"",
"test:watch": "vitest",
"test": "vitest run"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"dependencies": {
"@versini/ui-hooks": "3.0.0",
"uuid": "10.0.0"
}
}
25 changes: 25 additions & 0 deletions packages/auth-provider/src/components/AuthProvider/AuthContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createContext } from "react";

export type AuthContextType = {
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
isAuthenticated: boolean;
accessToken?: string;
refreshToken?: string;
idToken?: string;
logoutReason?: string;
};

const stub = (): never => {
throw new Error("You forgot to wrap your component in <AuthProvider>.");
};

export const AuthContext = createContext<AuthContextType>({
isAuthenticated: false,
login: stub,
logout: stub,
accessToken: undefined,
refreshToken: undefined,
idToken: undefined,
logoutReason: "",
});
159 changes: 159 additions & 0 deletions packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useLocalStorage } from "@versini/ui-hooks";
import { useEffect, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";

import { AuthContext } from "./AuthContext";

type AuthState = {
isAuthenticated: boolean;
idToken: string;
logoutReason: string;
userId: string;
accessToken?: string;
refreshToken?: string;
};
export const AUTH_TYPES = {
ID_TOKEN: "id_token",
};
const EXPIRED_SESSION =
"Oops! It looks like your session has expired. For your security, please log in again to continue.";

const serviceCall = async ({ params = {} }: { params?: any }) => {
try {
const nonce = uuidv4();
const response = await fetch(
`${process.env.PUBLIC_AUTH_SERVER_URL}/authenticate`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Auth-TenantId": `${params.tenantId}`,
},
body: JSON.stringify({ ...params, nonce }),
},
);

if (response.status !== 200) {
return { status: response.status, data: [] };
}
const { data, errors } = await response.json();
if (data.nonce !== nonce) {
return { status: 500, data: [] };
}

return {
status: response.status,
data,
errors,
};
} catch (_error) {
console.error(_error);
return { status: 500, data: [] };
}
};

function usePrevious<T>(state: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = state;
});
return ref.current;
}

export const AuthProvider = ({
children,
sessionExpiration,
tenantId,
accessType,
}: {
children: React.ReactNode;
sessionExpiration?: string;
tenantId: string;
accessType?: string;
}) => {
const [accessToken, setAccessToken, removeAccessToken] = useLocalStorage(
`@@auth@@::${tenantId}::@@access@@`,
"",
);
const [refreshToken, setRefreshToken, removeRefreshToken] = useLocalStorage(
`@@auth@@::${tenantId}::@@refresh@@`,
"",
);
const [idToken, setIdToken, removeIdToken] = useLocalStorage(
`@@auth@@::${tenantId}::@@user@@`,
"",
);
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: !!idToken,
accessToken,
refreshToken,
idToken,
logoutReason: "",
userId: "",
});

const previousIdToken = usePrevious(idToken) || "";

useEffect(() => {
if (previousIdToken !== idToken && idToken !== "") {
setAuthState({
isAuthenticated: true,
accessToken,
refreshToken,
idToken,
logoutReason: "",
userId: authState.userId,
});
} else if (previousIdToken !== idToken && idToken === "") {
setAuthState({
isAuthenticated: false,
accessToken: "",
refreshToken: "",
idToken: "",
logoutReason: EXPIRED_SESSION,
userId: "",
});
}
}, [accessToken, refreshToken, idToken, previousIdToken, authState.userId]);

const login = async (username: string, password: string) => {
const response = await serviceCall({
params: {
type: accessType || AUTH_TYPES.ID_TOKEN,
username,
password,
sessionExpiration,
tenantId,
},
});

if (response.data?.idToken) {
setIdToken(response.data.idToken);
response.data.accessToken && setAccessToken(response.data.accessToken);
response.data.refreshToken && setRefreshToken(response.data.refreshToken);
setAuthState({
isAuthenticated: true,
idToken: response.data.idToken,
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
userId: response.data.userId,
logoutReason: "",
});
return true;
}
return false;
};

const logout = () => {
removeAccessToken();
removeRefreshToken();
removeIdToken();
};

return (
<AuthContext.Provider value={{ ...authState, login, logout }}>
{children}
</AuthContext.Provider>
);
};
2 changes: 2 additions & 0 deletions packages/auth-provider/src/components/AuthProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AuthProvider } from "./AuthProvider";
export { useAuth } from "./useAuth";
5 changes: 5 additions & 0 deletions packages/auth-provider/src/components/AuthProvider/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useContext } from "react";
import { AuthContext, type AuthContextType } from "./AuthContext";

export const useAuth = (context = AuthContext): AuthContextType =>
useContext(context) as AuthContextType;
2 changes: 2 additions & 0 deletions packages/auth-provider/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AuthProvider } from "./AuthProvider/AuthProvider";
export { useAuth } from "./AuthProvider/useAuth";
1 change: 1 addition & 0 deletions packages/auth-provider/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
10 changes: 10 additions & 0 deletions packages/auth-provider/stats/stats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"0.0.0": {
"dist/index.js": {
"fileSize": 6711,
"fileSizeGzip": 2552,
"limit": "3 kb",
"passed": true
}
}
}
26 changes: 26 additions & 0 deletions packages/auth-provider/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vitest/globals", "@testing-library/jest-dom"],

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
10 changes: 10 additions & 0 deletions packages/auth-provider/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["./vite.config.ts", "./vitest.setup.ts"]
}
12 changes: 12 additions & 0 deletions packages/auth-provider/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";

export default defineConfig({
format: "esm",
entry: {
index: "src/components/index.ts",
},
outDir: "dist",
dts: {
only: true,
},
});
Loading

0 comments on commit 0c18c5c

Please sign in to comment.