mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
feat: enhance API and AuthApi with credentials support and async storage handling
- Added `credentials` option to `ApiOptions` and `BaseModuleApiOptions` for better request handling. - Updated `AuthApi` to pass `verified` status during token updates. - Refactored storage handling in `Api` to support async operations using a Proxy. - Improved `Authenticator` to handle cookie domain configuration and JSON request detection. - Adjusted `useAuth` to ensure logout and verify methods return promises for better async handling. - Fixed navigation URL construction in `useNavigate` and updated context menu actions in `_data.root.tsx`.
This commit is contained in:
@@ -40,6 +40,7 @@ export type ApiOptions = {
|
||||
data?: SubApiOptions<DataApiOptions>;
|
||||
auth?: SubApiOptions<AuthApiOptions>;
|
||||
media?: SubApiOptions<MediaApiOptions>;
|
||||
credentials?: RequestCredentials;
|
||||
} & (
|
||||
| {
|
||||
token?: string;
|
||||
@@ -67,7 +68,7 @@ export class Api {
|
||||
public auth!: AuthApi;
|
||||
public media!: MediaApi;
|
||||
|
||||
constructor(private options: ApiOptions = {}) {
|
||||
constructor(public options: ApiOptions = {}) {
|
||||
// only mark verified if forced
|
||||
this.verified = options.verified === true;
|
||||
|
||||
@@ -129,29 +130,45 @@ export class Api {
|
||||
} else if (this.storage) {
|
||||
this.storage.getItem(this.tokenKey).then((token) => {
|
||||
this.token_transport = "header";
|
||||
this.updateToken(token ? String(token) : undefined);
|
||||
this.updateToken(token ? String(token) : undefined, {
|
||||
verified: true,
|
||||
trigger: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make storage async to allow async storages even if sync given
|
||||
* @private
|
||||
*/
|
||||
private get storage() {
|
||||
if (!this.options.storage) return null;
|
||||
return {
|
||||
getItem: async (key: string) => {
|
||||
return await this.options.storage!.getItem(key);
|
||||
const storage = this.options.storage;
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return (...args: any[]) => {
|
||||
const response = storage ? storage[prop](...args) : undefined;
|
||||
if (response instanceof Promise) {
|
||||
return response;
|
||||
}
|
||||
return {
|
||||
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
||||
then: (fn) => fn(response),
|
||||
};
|
||||
};
|
||||
},
|
||||
},
|
||||
setItem: async (key: string, value: string) => {
|
||||
return await this.options.storage!.setItem(key, value);
|
||||
},
|
||||
removeItem: async (key: string) => {
|
||||
return await this.options.storage!.removeItem(key);
|
||||
},
|
||||
};
|
||||
) as any;
|
||||
}
|
||||
|
||||
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||
updateToken(
|
||||
token?: string,
|
||||
opts?: { rebuild?: boolean; verified?: boolean; trigger?: boolean },
|
||||
) {
|
||||
this.token = token;
|
||||
this.verified = false;
|
||||
this.verified = opts?.verified === true;
|
||||
|
||||
if (token) {
|
||||
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||
@@ -159,21 +176,22 @@ export class Api {
|
||||
this.user = undefined;
|
||||
}
|
||||
|
||||
const emit = () => {
|
||||
if (opts?.trigger !== false) {
|
||||
this.options.onAuthStateChange?.(this.getAuthState());
|
||||
}
|
||||
};
|
||||
if (this.storage) {
|
||||
const key = this.tokenKey;
|
||||
|
||||
if (token) {
|
||||
this.storage.setItem(key, token).then(() => {
|
||||
this.options.onAuthStateChange?.(this.getAuthState());
|
||||
});
|
||||
this.storage.setItem(key, token).then(emit);
|
||||
} else {
|
||||
this.storage.removeItem(key).then(() => {
|
||||
this.options.onAuthStateChange?.(this.getAuthState());
|
||||
});
|
||||
this.storage.removeItem(key).then(emit);
|
||||
}
|
||||
} else {
|
||||
if (opts?.trigger !== false) {
|
||||
this.options.onAuthStateChange?.(this.getAuthState());
|
||||
emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +200,7 @@ export class Api {
|
||||
|
||||
private markAuthVerified(verfied: boolean) {
|
||||
this.verified = verfied;
|
||||
this.options.onAuthStateChange?.(this.getAuthState());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -208,11 +227,6 @@ export class Api {
|
||||
}
|
||||
|
||||
async verifyAuth() {
|
||||
if (!this.token) {
|
||||
this.markAuthVerified(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { ok, data } = await this.auth.me();
|
||||
const user = data?.user;
|
||||
@@ -221,10 +235,10 @@ export class Api {
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
this.markAuthVerified(true);
|
||||
} catch (e) {
|
||||
this.markAuthVerified(false);
|
||||
this.updateToken(undefined);
|
||||
} finally {
|
||||
this.markAuthVerified(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +253,7 @@ export class Api {
|
||||
headers: this.options.headers,
|
||||
token_transport: this.token_transport,
|
||||
verbose: this.options.verbose,
|
||||
credentials: this.options.credentials,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,10 +272,9 @@ export class Api {
|
||||
this.auth = new AuthApi(
|
||||
{
|
||||
...baseParams,
|
||||
credentials: this.options.storage ? "omit" : "include",
|
||||
...this.options.auth,
|
||||
onTokenUpdate: (token) => {
|
||||
this.updateToken(token, { rebuild: true });
|
||||
onTokenUpdate: (token, verified) => {
|
||||
this.updateToken(token, { rebuild: true, verified, trigger: true });
|
||||
this.options.auth?.onTokenUpdate?.(token);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AuthResponse, SafeUser, AuthStrategy } from "bknd";
|
||||
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
||||
|
||||
export type AuthApiOptions = BaseModuleApiOptions & {
|
||||
onTokenUpdate?: (token?: string) => void | Promise<void>;
|
||||
onTokenUpdate?: (token?: string, verified?: boolean) => void | Promise<void>;
|
||||
credentials?: "include" | "same-origin" | "omit";
|
||||
};
|
||||
|
||||
@@ -17,23 +17,19 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||
}
|
||||
|
||||
async login(strategy: string, input: any) {
|
||||
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
||||
credentials: this.options.credentials,
|
||||
});
|
||||
const res = await this.post<AuthResponse>([strategy, "login"], input);
|
||||
|
||||
if (res.ok && res.body.token) {
|
||||
await this.options.onTokenUpdate?.(res.body.token);
|
||||
await this.options.onTokenUpdate?.(res.body.token, true);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async register(strategy: string, input: any) {
|
||||
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
||||
credentials: this.options.credentials,
|
||||
});
|
||||
const res = await this.post<AuthResponse>([strategy, "register"], input);
|
||||
|
||||
if (res.ok && res.body.token) {
|
||||
await this.options.onTokenUpdate?.(res.body.token);
|
||||
await this.options.onTokenUpdate?.(res.body.token, true);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -71,6 +67,11 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.options.onTokenUpdate?.(undefined);
|
||||
return this.get(["logout"], undefined, {
|
||||
headers: {
|
||||
// this way bknd detects a json request and doesn't redirect back
|
||||
Accept: "application/json",
|
||||
},
|
||||
}).then(() => this.options.onTokenUpdate?.(undefined, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface UserPool {
|
||||
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
||||
export const cookieConfig = s
|
||||
.strictObject({
|
||||
domain: s.string().optional(),
|
||||
path: s.string({ default: "/" }),
|
||||
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
||||
secure: s.boolean({ default: true }),
|
||||
@@ -290,6 +291,7 @@ export class Authenticator<
|
||||
|
||||
return {
|
||||
...cookieConfig,
|
||||
domain: cookieConfig.domain ?? undefined,
|
||||
expires: new Date(Date.now() + expires * 1000),
|
||||
};
|
||||
}
|
||||
@@ -354,7 +356,10 @@ export class Authenticator<
|
||||
|
||||
// @todo: move this to a server helper
|
||||
isJsonRequest(c: Context): boolean {
|
||||
return c.req.header("Content-Type") === "application/json";
|
||||
return (
|
||||
c.req.header("Content-Type") === "application/json" ||
|
||||
c.req.header("Accept") === "application/json"
|
||||
);
|
||||
}
|
||||
|
||||
async getBody(c: Context) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type BaseModuleApiOptions = {
|
||||
host: string;
|
||||
basepath?: string;
|
||||
token?: string;
|
||||
credentials?: RequestCredentials;
|
||||
headers?: Headers;
|
||||
token_transport?: "header" | "cookie" | "none";
|
||||
verbose?: boolean;
|
||||
@@ -106,6 +107,7 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
|
||||
|
||||
const request = new Request(url, {
|
||||
..._init,
|
||||
credentials: this.options.credentials,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
|
||||
@@ -52,11 +52,16 @@ export class AppServer extends Module<AppServerConfig> {
|
||||
}
|
||||
|
||||
override async build() {
|
||||
const origin = this.config.cors.origin ?? "";
|
||||
const origin = this.config.cors.origin ?? "*";
|
||||
const origins = origin.includes(",") ? origin.split(",").map((o) => o.trim()) : [origin];
|
||||
const all_origins = origins.includes("*");
|
||||
this.client.use(
|
||||
"*",
|
||||
cors({
|
||||
origin: origin.includes(",") ? origin.split(",").map((o) => o.trim()) : origin,
|
||||
origin: (origin: string) => {
|
||||
if (all_origins) return origin;
|
||||
return origins.includes(origin) ? origin : undefined;
|
||||
},
|
||||
allowMethods: this.config.cors.allow_methods,
|
||||
allowHeaders: this.config.cors.allow_headers,
|
||||
credentials: this.config.cors.allow_credentials,
|
||||
|
||||
@@ -44,18 +44,17 @@ export const ClientProvider = ({
|
||||
...apiProps,
|
||||
verbose: isDebug(),
|
||||
onAuthStateChange: (state) => {
|
||||
const { token, ...rest } = state;
|
||||
props.onAuthStateChange?.(state);
|
||||
if (!authState?.token || state.token !== authState?.token) {
|
||||
setAuthState(state);
|
||||
if (!authState?.token || token !== authState?.token) {
|
||||
setAuthState(rest);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[JSON.stringify(apiProps)],
|
||||
);
|
||||
|
||||
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(
|
||||
apiProps.user ? api.getAuthState() : undefined,
|
||||
);
|
||||
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(api.getAuthState());
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api, authState }}>
|
||||
|
||||
@@ -16,8 +16,8 @@ type UseAuth = {
|
||||
verified: boolean;
|
||||
login: (data: LoginData) => Promise<AuthResponse>;
|
||||
register: (data: LoginData) => Promise<AuthResponse>;
|
||||
logout: () => void;
|
||||
verify: () => void;
|
||||
logout: () => Promise<void>;
|
||||
verify: () => Promise<void>;
|
||||
setToken: (token: string) => void;
|
||||
};
|
||||
|
||||
@@ -42,12 +42,13 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
api.updateToken(undefined);
|
||||
invalidate();
|
||||
await api.auth.logout();
|
||||
await invalidate();
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
await api.verifyAuth();
|
||||
await invalidate();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -95,7 +95,7 @@ export function useNavigate() {
|
||||
window.location.href = url;
|
||||
return;
|
||||
} else if ("target" in options) {
|
||||
const _url = window.location.origin + basepath + router.base + url;
|
||||
const _url = window.location.origin + router.base + url;
|
||||
window.open(_url, options.target);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,9 @@ const EntityContextMenu = ({
|
||||
href && {
|
||||
icon: IconExternalLink,
|
||||
label: "Open in tab",
|
||||
onClick: () => navigate(href, { target: "_blank" }),
|
||||
onClick: () => {
|
||||
navigate(href, { target: "_blank", absolute: true });
|
||||
},
|
||||
},
|
||||
separator,
|
||||
!$data.system(entity.name).any && {
|
||||
|
||||
Reference in New Issue
Block a user