react-oauth-providers 1.0.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/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # React OAuth Providers
2
+
3
+ A lightweight React package for OAuth authentication. Currently, only **Google** is supported.
4
+
5
+ ## ⚠️ **Notes**
6
+
7
+ - This package is designed for **SPA applications**.
8
+ - Google **UWP clients** are supported.
9
+ - Passing a **client secret** from the frontend is **vulnerable** and should be avoided.
10
+ - Configure **spaCallbackUri** correctly to match your route.
11
+
12
+ ---
13
+
14
+ <br>
15
+ <br>
16
+
17
+ ## 📦 Installation
18
+
19
+ ```bash
20
+ npm install react-oauth-providers@latest
21
+ ```
22
+
23
+ <br>
24
+ <br>
25
+
26
+ ## ⚙️ Setup Google OAuth
27
+
28
+ Go to [Google Cloud Console](https://console.cloud.google.com/) and create a new app.
29
+
30
+ Create an OAuth client and set **Application type** as **Universal Windows Platform (UWP)**.
31
+
32
+ <br>
33
+ <br>
34
+
35
+ ## 🚀 Quick Start
36
+
37
+ ```js
38
+ import { StrictMode } from 'react'
39
+ import { createRoot } from 'react-dom/client'
40
+ import { AuthProvider, type IAuthConfig } from 'react-oauth-providers'
41
+ import App from './App'
42
+
43
+ const authConfig: IAuthConfig = {
44
+ googleProvider: {
45
+ clientId: process.env.GOOGLE_CLIENT_ID,
46
+ },
47
+ storage: 'session', // 'session' | 'local'
48
+ spaCallbackUri: `${window.location.origin}/auth/callback`,
49
+ preLogin: () => console.log('Before login...'),
50
+ postLogin: async () => {
51
+ console.log('After login...')
52
+ window.location.href = window.location.origin
53
+ },
54
+ }
55
+
56
+ createRoot(document.getElementById('root')!).render(
57
+ <StrictMode>
58
+ <AuthProvider authConfig={authConfig}>
59
+ <App />
60
+ </AuthProvider>
61
+ </StrictMode>
62
+ )
63
+ ```
64
+
65
+ <br>
66
+ <br>
67
+
68
+ ## 🔑 Using Authentication in Components
69
+
70
+ ```js
71
+ import { useAuth } from "@/providers/Auth";
72
+
73
+ export default function Login() {
74
+ const { loginInProgress, user, logOut, signinWithGoogle } = useAuth();
75
+
76
+ return (
77
+ <div>
78
+ {user ? (
79
+ <>
80
+ <p>Welcome, {user.name}</p>
81
+ <button onClick={logOut}>Logout</button>
82
+ </>
83
+ ) : (
84
+ <button
85
+ onClick={async () => await signinWithGoogle()}
86
+ disabled={loginInProgress}
87
+ >
88
+ {loginInProgress ? "Signing in..." : "Sign in with Google"}
89
+ </button>
90
+ )}
91
+ </div>
92
+ );
93
+ }
94
+ ```
95
+
96
+ <br>
97
+ <br>
98
+
99
+ ## ✅ Features
100
+
101
+ - **Google OAuth** (no backend secret required)
102
+ - **Session or Local storage** support
103
+ - **preLogin** & **postLogin** hooks for custom logic
104
+ - **Automatic token management**: token storage, decoding, and refresh are fully handled by the package
105
+ - Fully **React hooks-based** API (useAuth)
106
+
107
+ <br>
108
+
109
+ ## 🧩 AuthProvider Props
110
+
111
+ | Prop | Type | Description |
112
+ | -------------- | -------------------------------------- | --------------------------------------------------------- |
113
+ | googleProvider | `{ clientId: string, scope?: string }` | Your Google OAuth client configuration |
114
+ | storage | `"session" \| "local"` | Storage type for session or local storage |
115
+ | spaCallbackUri | `string` | SPA callback URL for OAuth redirects |
116
+ | preLogin | `() => void` | Function called before login starts |
117
+ | postLogin | `() => void \| Promise<void>` | Function called after login completes |
118
+ | onLogInError | `(error) => void` | Function called when an error occurs in the login process |
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "react-oauth-providers",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.cjs.js",
5
+ "module": "dist/index.esm.js",
6
+ "types": "dist/types/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.tsx --format cjs,esm --dts --out-dir dist",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "dependencies": {
12
+ "oauth-pkce": "^0.0.7"
13
+ },
14
+ "peerDependencies": {
15
+ "react": "^18.0.0",
16
+ "react-dom": "^18.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.0.0",
20
+ "@types/react-dom": "^18.0.0",
21
+ "tsup": "^7.0.0",
22
+ "typescript": "^5.0.0"
23
+ },
24
+ "keywords": [
25
+ "oauth-providers",
26
+ "react-oauth-providers",
27
+ "google-oauth-provider"
28
+ ],
29
+ "author": "Asif Sorowar"
30
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react'
2
+ import type { IAuthContext } from './types'
3
+
4
+ export const AuthContext = createContext({} as IAuthContext)
@@ -0,0 +1,103 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { AuthContext } from "./AuthContext";
3
+ import * as actions from "./actions";
4
+ import type { IAuthProvider, ILogoutFunc, IAuthInternalConfig } from "./types";
5
+ import { createInternalConfig } from "./authConfig";
6
+ import { getSessionItem } from "./utils/session";
7
+
8
+ export const AuthProvider: React.FC<IAuthProvider> = ({
9
+ authConfig,
10
+ children,
11
+ }) => {
12
+ const config = useMemo<IAuthInternalConfig>(
13
+ () => createInternalConfig(authConfig),
14
+ [authConfig]
15
+ );
16
+ const tmpAuthProviderType = useMemo(
17
+ () => getSessionItem(config.keys.PROVIDER_TYPE),
18
+ [config]
19
+ );
20
+
21
+ const [loginInProgress, setLoginInProgress] = useState(true);
22
+ const [user, setUser] = useState<null | any>();
23
+
24
+ useEffect(() => {
25
+ actions.handleFetchSessionUser().then((user) => {
26
+ setUser(user);
27
+ setLoginInProgress(false);
28
+ });
29
+ }, []);
30
+
31
+ const signinWithGoogle = useCallback(async () => {
32
+ if (!config.googleProvider?.clientId) {
33
+ throw Error(
34
+ "'clientId' must be set in the 'googleProvider' object in the AuthProvider Config"
35
+ );
36
+ }
37
+
38
+ config.preLogin?.();
39
+
40
+ try {
41
+ if (config.googleProvider.type === "spa") {
42
+ await actions.signinWithGoogleSpa();
43
+ }
44
+ } catch (error) {
45
+ config.onLogInError?.(error);
46
+ }
47
+ }, []);
48
+
49
+ // callback handler for spa login
50
+ useEffect(() => {
51
+ if (!config || !tmpAuthProviderType) return;
52
+ if (window.location.pathname !== new URL(config.spaCallbackUri!).pathname) {
53
+ return;
54
+ }
55
+
56
+ const callback = async () => {
57
+ try {
58
+ await actions.handleCallback();
59
+
60
+ config.postLogin?.();
61
+ } catch (error) {
62
+ config.onLogInError?.(error);
63
+ }
64
+ };
65
+
66
+ callback();
67
+ }, [tmpAuthProviderType, config]);
68
+
69
+ const logOut = useCallback<ILogoutFunc>(
70
+ ({ logoutHint, redirect = true, redirectUri } = {}) => {
71
+ actions.logout();
72
+
73
+ if (!redirect) return;
74
+
75
+ // Determine redirect URL or fallback to root
76
+ const baseUrl = redirectUri || config.logoutRedirect || "/";
77
+
78
+ const params = new URLSearchParams();
79
+ if (logoutHint) params.append("logout_hint", logoutHint);
80
+
81
+ // Append params only if there are any
82
+ const url = params.toString()
83
+ ? `${baseUrl}?${params.toString()}`
84
+ : baseUrl;
85
+
86
+ window.location.assign(url);
87
+ },
88
+ []
89
+ );
90
+
91
+ return (
92
+ <AuthContext.Provider
93
+ value={{
94
+ loginInProgress,
95
+ signinWithGoogle,
96
+ user,
97
+ logOut,
98
+ }}
99
+ >
100
+ {children}
101
+ </AuthContext.Provider>
102
+ );
103
+ };
package/src/actions.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { getSessionItem } from "./utils/session";
2
+ import {
3
+ getGoogleSessionUser,
4
+ signinWithGoogleCallback,
5
+ refreshGoogleAccessToken,
6
+ } from "./google";
7
+ import { getConfig } from "./authConfig";
8
+ import { AUTH_PROVIDERS } from "./utils/constants";
9
+ import { getFromStorage, removeFromStorage } from "./utils/storage";
10
+
11
+ export { signinWithGoogleSpa as signinWithGoogleSpa } from "./google";
12
+
13
+ let refreshTimeout: ReturnType<typeof setTimeout> | null = null;
14
+
15
+ export const handleCallback = async () => {
16
+ const config = getConfig();
17
+ if (!config) return;
18
+
19
+ const keys = config?.keys;
20
+
21
+ const authMode = getSessionItem(keys.PROVIDER);
22
+ const verifier = getSessionItem(keys.VERIFIER);
23
+ if (!authMode || !verifier) return;
24
+
25
+ try {
26
+ switch (authMode) {
27
+ case AUTH_PROVIDERS.google: {
28
+ const searchQuery = new URLSearchParams(window.location.search);
29
+ const code = searchQuery.get("code");
30
+ if (!code) return;
31
+
32
+ await signinWithGoogleCallback(code, verifier);
33
+ break;
34
+ }
35
+
36
+ default:
37
+ break;
38
+ }
39
+ } catch (error) {
40
+ return Promise.reject(error);
41
+ }
42
+ };
43
+
44
+ export const handleFetchSessionUser = async (retry = true) => {
45
+ const config = getConfig();
46
+ if (!config) return;
47
+
48
+ const keys = config?.keys;
49
+
50
+ const authMode = getFromStorage(config.storage!, keys.PROVIDER);
51
+ if (!authMode) return;
52
+
53
+ let sessionUser = null;
54
+
55
+ try {
56
+ switch (authMode) {
57
+ case AUTH_PROVIDERS.google: {
58
+ sessionUser = await getGoogleSessionUser();
59
+ break;
60
+ }
61
+
62
+ default:
63
+ break;
64
+ }
65
+
66
+ if (sessionUser) handleScheduleTokenRefresh();
67
+
68
+ return sessionUser;
69
+ } catch (error) {
70
+ console.log("Fetch Session User Error:", error);
71
+
72
+ if (!retry) return null;
73
+
74
+ switch (authMode) {
75
+ case AUTH_PROVIDERS.google: {
76
+ await refreshGoogleAccessToken();
77
+ await handleFetchSessionUser(false);
78
+
79
+ break;
80
+ }
81
+
82
+ default:
83
+ break;
84
+ }
85
+
86
+ return null;
87
+ }
88
+ };
89
+
90
+ const handleScheduleTokenRefresh = () => {
91
+ const config = getConfig();
92
+ if (!config) return;
93
+
94
+ const keys = config?.keys;
95
+
96
+ const authMode = getFromStorage(config.storage!, keys.PROVIDER);
97
+ if (!authMode) return;
98
+
99
+ const expiresIn = Number(
100
+ getFromStorage(config.storage!, keys.EXPIRES_IN) ?? 0
101
+ );
102
+ if (!expiresIn) return;
103
+
104
+ clearTokenRefresh();
105
+
106
+ // Refresh 60 seconds before expiry
107
+ const refreshTime = expiresIn - 60 * 1000;
108
+ refreshTimeout = setTimeout(async () => {
109
+ switch (authMode) {
110
+ case AUTH_PROVIDERS.google: {
111
+ await refreshGoogleAccessToken();
112
+ break;
113
+ }
114
+
115
+ default:
116
+ break;
117
+ }
118
+ }, refreshTime);
119
+ };
120
+
121
+ const clearTokenRefresh = () => {
122
+ if (refreshTimeout) {
123
+ clearTimeout(refreshTimeout);
124
+ refreshTimeout = null;
125
+ }
126
+ };
127
+
128
+ export const logout = () => {
129
+ clearTokens();
130
+ clearTokenRefresh();
131
+ };
132
+
133
+ const clearTokens = () => {
134
+ const config = getConfig();
135
+ if (!config) return;
136
+
137
+ const keys = config?.keys;
138
+
139
+ const {
140
+ PROVIDER,
141
+ ACCESS_TOKEN,
142
+ EXPIRES_IN,
143
+ REFRESH_TOKEN,
144
+ TOKEN_TYPE,
145
+ ID_TOKEN,
146
+ PROVIDER_TYPE,
147
+ } = keys;
148
+
149
+ removeFromStorage(config.storage!, PROVIDER);
150
+ removeFromStorage(config.storage!, PROVIDER_TYPE);
151
+ removeFromStorage(config.storage!, ACCESS_TOKEN);
152
+ removeFromStorage(config.storage!, EXPIRES_IN);
153
+ removeFromStorage(config.storage!, REFRESH_TOKEN);
154
+ removeFromStorage(config.storage!, TOKEN_TYPE);
155
+ removeFromStorage(config.storage!, ID_TOKEN);
156
+ };
@@ -0,0 +1,56 @@
1
+ import type { IAuthConfig, IAuthInternalConfig, Keys } from "./types";
2
+ import { toURL, makeKey } from "./utils/others";
3
+
4
+ export let config: IAuthInternalConfig | null = null;
5
+
6
+ export const getConfig = () => config;
7
+
8
+ export function createInternalConfig(
9
+ passedConfig: IAuthConfig
10
+ ): IAuthInternalConfig {
11
+ // Set default values for internal config object
12
+ const {
13
+ preLogin = () => null,
14
+ postLogin = () => null,
15
+ onLogInError = () => null,
16
+ storage = "local",
17
+ spaCallbackUri = passedConfig.spaCallbackUri
18
+ ? toURL(passedConfig.spaCallbackUri).toString()
19
+ : window.location.origin,
20
+ logoutRedirect = passedConfig.logoutRedirect
21
+ ? toURL(passedConfig.logoutRedirect).toString()
22
+ : window.location.origin,
23
+ storageKeyPrefix = "auth_",
24
+ googleProvider,
25
+ }: IAuthConfig = passedConfig;
26
+
27
+ config = {
28
+ ...passedConfig,
29
+ preLogin,
30
+ postLogin,
31
+ onLogInError,
32
+ storage,
33
+ storageKeyPrefix,
34
+ spaCallbackUri,
35
+ logoutRedirect,
36
+ keys: createAuthKeys(storageKeyPrefix),
37
+ googleProvider: googleProvider
38
+ ? { type: "spa", scope: "openid email profile", ...googleProvider }
39
+ : undefined,
40
+ };
41
+
42
+ return config;
43
+ }
44
+
45
+ export function createAuthKeys(prefix?: string): Record<keyof Keys, string> {
46
+ return {
47
+ PROVIDER_TYPE: makeKey("provider_type", prefix),
48
+ PROVIDER: makeKey("provider", prefix),
49
+ VERIFIER: makeKey("verifier", prefix),
50
+ ACCESS_TOKEN: makeKey("access_token", prefix),
51
+ EXPIRES_IN: makeKey("expires_in", prefix),
52
+ REFRESH_TOKEN: makeKey("refresh_token", prefix),
53
+ TOKEN_TYPE: makeKey("token_type", prefix),
54
+ ID_TOKEN: makeKey("id_token", prefix),
55
+ };
56
+ }
@@ -0,0 +1,157 @@
1
+ import http from "../utils/http";
2
+ import { removeSessionItem, setSessionItem } from "../utils/session";
3
+ import { generatePkce } from "../utils/pkce";
4
+ import type { IAuthGoogleProviderType } from "../types";
5
+ import { getConfig } from "../authConfig";
6
+ import { AUTH_PROVIDERS } from "../utils/constants";
7
+ import { getFromStorage, keepToStorage } from "../utils/storage";
8
+
9
+ export const signinWithGoogleSpa = async () => {
10
+ const config = getConfig();
11
+ if (!config) return;
12
+
13
+ const { googleProvider, keys } = config;
14
+
15
+ const { verifier, challenge, algorithm } = await generatePkce();
16
+
17
+ setSessionItem(keys.VERIFIER, verifier, 3 * 60 * 1000);
18
+ setSessionItem(keys.PROVIDER, AUTH_PROVIDERS.google, 3 * 60 * 1000);
19
+ setSessionItem(keys.PROVIDER_TYPE, googleProvider?.type!, 3 * 60 * 1000);
20
+
21
+ const params = new URLSearchParams({
22
+ client_id: googleProvider?.clientId!,
23
+ redirect_uri: config?.spaCallbackUri!,
24
+ response_type: "code",
25
+ scope: googleProvider?.scope || "",
26
+ code_challenge: challenge,
27
+ code_challenge_method: algorithm,
28
+ access_type: "offline",
29
+ prompt: "consent",
30
+ });
31
+
32
+ window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
33
+ };
34
+
35
+ export const signinWithGoogleCallback = async (
36
+ code: string,
37
+ verifier: string
38
+ ) => {
39
+ if (!code || !verifier) return;
40
+
41
+ const config = getConfig();
42
+ if (!config) return;
43
+
44
+ const { googleProvider, keys } = config;
45
+ const type: IAuthGoogleProviderType = "spa";
46
+
47
+ try {
48
+ const data = new URLSearchParams({
49
+ client_id: googleProvider?.clientId!,
50
+ grant_type: "authorization_code",
51
+ code: code,
52
+ redirect_uri: config?.spaCallbackUri!,
53
+ code_verifier: verifier,
54
+ });
55
+
56
+ const res = await http.post(
57
+ "https://oauth2.googleapis.com/token",
58
+ data.toString(),
59
+ {
60
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
61
+ }
62
+ );
63
+
64
+ const tokens = res.data as any;
65
+
66
+ if (tokens?.access_token) {
67
+ removeSessionItem(keys.PROVIDER);
68
+ removeSessionItem(keys.VERIFIER);
69
+ removeSessionItem(keys.PROVIDER_TYPE);
70
+
71
+ keepToStorage(config.storage!, keys.PROVIDER, AUTH_PROVIDERS.google);
72
+ keepToStorage(config.storage!, keys.PROVIDER_TYPE, type);
73
+ keepToStorage(config.storage!, keys.ACCESS_TOKEN, tokens.access_token);
74
+ keepToStorage(
75
+ config.storage!,
76
+ keys.EXPIRES_IN,
77
+ String(Date.now() + tokens.expires_in * 1000)
78
+ );
79
+ keepToStorage(config.storage!, keys.REFRESH_TOKEN, tokens.refresh_token);
80
+ keepToStorage(config.storage!, keys.TOKEN_TYPE, tokens.token_type);
81
+ keepToStorage(config.storage!, keys.ID_TOKEN, tokens.id_token);
82
+ }
83
+ } catch (error) {
84
+ return Promise.reject(error);
85
+ }
86
+ };
87
+
88
+ export const getGoogleSessionUser = async () => {
89
+ const config = getConfig();
90
+ if (!config) return;
91
+
92
+ const { keys } = config;
93
+
94
+ try {
95
+ const accessToken = getFromStorage(config.storage!, keys.ACCESS_TOKEN);
96
+ if (!accessToken) return;
97
+
98
+ const res = await http.get(
99
+ `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${accessToken}`
100
+ );
101
+
102
+ const user = res.data;
103
+
104
+ return user;
105
+ } catch (error) {
106
+ return Promise.reject(error);
107
+ }
108
+ };
109
+
110
+ export const refreshGoogleAccessToken = async () => {
111
+ const config = getConfig();
112
+ if (!config) return;
113
+
114
+ const { googleProvider, keys } = config;
115
+
116
+ const refreshToken = getFromStorage(config.storage!, keys.REFRESH_TOKEN);
117
+
118
+ if (!refreshToken) return;
119
+
120
+ try {
121
+ const data = new URLSearchParams({
122
+ client_id: googleProvider?.clientId!,
123
+ grant_type: "refresh_token",
124
+ refresh_token: refreshToken,
125
+ });
126
+
127
+ const res = await http.post(
128
+ "https://oauth2.googleapis.com/token",
129
+ data.toString(),
130
+ {
131
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
132
+ }
133
+ );
134
+
135
+ const tokens = res.data as any;
136
+
137
+ if (!tokens.access_token) {
138
+ console.error("Failed to refresh access token", tokens);
139
+ return null;
140
+ }
141
+
142
+ keepToStorage(config.storage!, keys.ACCESS_TOKEN, tokens.access_token);
143
+ if (tokens.expires_in) {
144
+ keepToStorage(
145
+ config.storage!,
146
+ keys.EXPIRES_IN,
147
+ String(Date.now() + tokens.expires_in * 1000)
148
+ );
149
+ }
150
+ if (tokens.id_token)
151
+ keepToStorage(config.storage!, keys.ID_TOKEN, tokens.id_token);
152
+ if (tokens.token_type)
153
+ keepToStorage(config.storage!, keys.TOKEN_TYPE, tokens.token_type);
154
+ } catch (error) {
155
+ return Promise.reject(error);
156
+ }
157
+ };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { AuthContext } from './AuthContext'
2
+ export { AuthProvider } from './AuthProvider'
3
+ export { useAuth } from './useAuth'
4
+
5
+ export type { IAuthContext, IAuthProvider, IAuthConfig } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface IAuthContext {
4
+ loginInProgress: boolean;
5
+ signinWithGoogle: () => Promise<void>;
6
+ logOut: ILogoutFunc;
7
+ user?: any;
8
+ error?: string | null;
9
+ }
10
+
11
+ export interface IAuthProvider {
12
+ authConfig: IAuthConfig;
13
+ children: ReactNode;
14
+ }
15
+
16
+ export type IAuthStorageType = "session" | "local";
17
+
18
+ export type IAuthGoogleProviderType = "spa";
19
+
20
+ export interface IAuthGoogleProvider {
21
+ clientId: string;
22
+ type?: IAuthGoogleProviderType;
23
+ scope?: string;
24
+ }
25
+
26
+ // Input from users of the package, some optional values
27
+ export type IAuthConfig = {
28
+ googleProvider?: IAuthGoogleProvider;
29
+ spaCallbackUri?: string;
30
+ logoutRedirect?: string;
31
+ preLogin?: () => void;
32
+ postLogin?: () => void;
33
+ onLogInError?: (err: any) => void;
34
+ storageKeyPrefix?: string;
35
+ storage?: IAuthStorageType;
36
+
37
+ error?: string | null;
38
+ };
39
+
40
+ export type Keys = {
41
+ PROVIDER_TYPE: string;
42
+ PROVIDER: string;
43
+ VERIFIER: string;
44
+ ACCESS_TOKEN: string;
45
+ EXPIRES_IN: string;
46
+ REFRESH_TOKEN: string;
47
+ TOKEN_TYPE: string;
48
+ ID_TOKEN: string;
49
+ };
50
+
51
+ export type IAuthInternalConfig = IAuthConfig & { keys: Keys };
52
+
53
+ export type ILogoutFunc = (args?: {
54
+ logoutHint?: string;
55
+ redirect?: boolean;
56
+ redirectUri?: string;
57
+ }) => void;
@@ -0,0 +1,13 @@
1
+ import { useContext } from 'react'
2
+ import { AuthContext } from './AuthContext'
3
+ import type { IAuthContext } from './types'
4
+
5
+ export function useAuth(): IAuthContext {
6
+ const ctx = useContext(AuthContext)
7
+
8
+ if (!ctx) {
9
+ throw new Error('useAuth must be used within an AuthProvider')
10
+ }
11
+
12
+ return ctx
13
+ }
@@ -0,0 +1 @@
1
+ export const AUTH_PROVIDERS = { google: 'google' } as const
@@ -0,0 +1,51 @@
1
+ type RequestOptions = {
2
+ headers?: Record<string, string>;
3
+ body?: string;
4
+ };
5
+
6
+ async function request(
7
+ url: string,
8
+ options: RequestOptions & { method: string }
9
+ ) {
10
+ let body: any;
11
+ const headers: Record<string, string> = { ...(options.headers ?? {}) };
12
+
13
+ try {
14
+ body = options.body;
15
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
16
+
17
+ const response = await fetch(url, {
18
+ method: options.method,
19
+ headers,
20
+ body,
21
+ });
22
+
23
+ if (!response.ok) {
24
+ const error = await response.json().catch(() => ({}));
25
+ const expectedError = response.status >= 400 && response.status < 500;
26
+
27
+ if (!expectedError) {
28
+ console.log({ error });
29
+ console.log("Unexpected error occurs");
30
+ }
31
+
32
+ return Promise.reject({ status: response.status, ...error });
33
+ }
34
+
35
+ return { data: await response.json() };
36
+ } catch (error) {
37
+ console.log("Network or unexpected error:", error);
38
+ return Promise.reject(error);
39
+ }
40
+ }
41
+
42
+ export default {
43
+ get: (url: string, options?: RequestOptions) =>
44
+ request(url, { ...options, method: "GET" }),
45
+ post: (url: string, body?: string, options?: RequestOptions) =>
46
+ request(url, { ...options, method: "POST", body }),
47
+ put: (url: string, body?: string, options?: RequestOptions) =>
48
+ request(url, { ...options, method: "PUT", body }),
49
+ delete: (url: string, options?: RequestOptions) =>
50
+ request(url, { ...options, method: "DELETE" }),
51
+ };
@@ -0,0 +1,13 @@
1
+ export const toURL = (input: string): URL => {
2
+ // If input is absolute, URL works directly
3
+ try {
4
+ return new URL(input)
5
+ } catch {
6
+ // If input is relative, use window.location.origin as base
7
+ return new URL(input, window.location.origin)
8
+ }
9
+ }
10
+
11
+ export const makeKey = (key: string, prefix?: string) => {
12
+ return prefix ? `${prefix}_${key}` : key
13
+ }
@@ -0,0 +1,15 @@
1
+ import getPkce from "oauth-pkce";
2
+
3
+ export const generatePkce = async (): Promise<{
4
+ verifier: string;
5
+ challenge: string;
6
+ algorithm: string;
7
+ }> => {
8
+ return new Promise((resolve, reject) => {
9
+ getPkce(50, (error, { verifier, challenge }) => {
10
+ if (error) reject(error);
11
+
12
+ resolve({ verifier, challenge, algorithm: "S256" });
13
+ });
14
+ });
15
+ };
@@ -0,0 +1,33 @@
1
+ export function setSessionItem(key: string, value: string, ttlMs: number) {
2
+ const data = {
3
+ value,
4
+ expires: Date.now() + ttlMs,
5
+ }
6
+
7
+ sessionStorage.setItem(key, JSON.stringify(data))
8
+ }
9
+
10
+ export function getSessionItem(key: string): string | null {
11
+ const dataStr = sessionStorage.getItem(key)
12
+ if (!dataStr) return null
13
+
14
+ try {
15
+ const data = JSON.parse(dataStr)
16
+ if (Date.now() > data.expires) {
17
+ sessionStorage.removeItem(key)
18
+ return null
19
+ }
20
+
21
+ return data.value
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ export function removeSessionItem(key: string) {
28
+ try {
29
+ sessionStorage.removeItem(key)
30
+ } catch (error) {
31
+ return null
32
+ }
33
+ }
@@ -0,0 +1,18 @@
1
+ import type { IAuthStorageType } from '../types'
2
+
3
+ export const keepToStorage = (type: IAuthStorageType, key: string, value: string) => {
4
+ if (type === 'local') localStorage.setItem(key, value)
5
+ else if (type === 'session') sessionStorage.setItem(key, value)
6
+ }
7
+
8
+ export const removeFromStorage = (type: IAuthStorageType, key: string) => {
9
+ if (type === 'local') localStorage.removeItem(key)
10
+ else if (type === 'session') sessionStorage.removeItem(key)
11
+ }
12
+
13
+ export const getFromStorage = (type: IAuthStorageType, key: string) => {
14
+ if (type === 'local') return localStorage.getItem(key) ?? ''
15
+ else if (type === 'session') return sessionStorage.getItem(key) ?? ''
16
+
17
+ return ''
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "ESNext",
5
+ "declaration": true,
6
+ "declarationDir": "dist/types",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "jsx": "react",
10
+ "moduleResolution": "node",
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src"]
15
+ }