mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
api: added custom storage option (#174)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ packages/media/.env
|
|||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
.debug
|
||||||
|
.history
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { SafeUser } from "auth";
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "core/utils";
|
import { omitKeys } from "core/utils";
|
||||||
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
@@ -21,14 +22,24 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubApiOptions<T extends BaseModuleApiOptions> = Omit<T, keyof BaseModuleApiOptions>;
|
||||||
|
|
||||||
export type ApiOptions = {
|
export type ApiOptions = {
|
||||||
host?: string;
|
host?: string;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
key?: string;
|
key?: string;
|
||||||
localStorage?: boolean;
|
storage?: {
|
||||||
|
getItem: (key: string) => string | undefined | null | Promise<string | undefined | null>;
|
||||||
|
setItem: (key: string, value: string) => void | Promise<void>;
|
||||||
|
removeItem: (key: string) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
onAuthStateChange?: (state: AuthState) => void;
|
||||||
fetcher?: ApiFetcher;
|
fetcher?: ApiFetcher;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
verified?: boolean;
|
verified?: boolean;
|
||||||
|
data?: SubApiOptions<DataApiOptions>;
|
||||||
|
auth?: SubApiOptions<AuthApiOptions>;
|
||||||
|
media?: SubApiOptions<MediaApiOptions>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -61,18 +72,18 @@ export class Api {
|
|||||||
this.verified = options.verified === true;
|
this.verified = options.verified === true;
|
||||||
|
|
||||||
// prefer request if given
|
// prefer request if given
|
||||||
if ("request" in options) {
|
if ("request" in options && options.request) {
|
||||||
this.options.host = options.host ?? new URL(options.request.url).origin;
|
this.options.host = options.host ?? new URL(options.request.url).origin;
|
||||||
this.options.headers = options.headers ?? options.request.headers;
|
this.options.headers = options.headers ?? options.request.headers;
|
||||||
this.extractToken();
|
this.extractToken();
|
||||||
|
|
||||||
// then check for a token
|
// then check for a token
|
||||||
} else if ("token" in options) {
|
} else if ("token" in options && options.token) {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(options.token);
|
this.updateToken(options.token, { trigger: false });
|
||||||
|
|
||||||
// then check for an user object
|
// then check for an user object
|
||||||
} else if ("user" in options) {
|
} else if ("user" in options && options.user) {
|
||||||
this.token_transport = "none";
|
this.token_transport = "none";
|
||||||
this.user = options.user;
|
this.user = options.user;
|
||||||
this.verified = options.verified !== false;
|
this.verified = options.verified !== false;
|
||||||
@@ -115,16 +126,30 @@ export class Api {
|
|||||||
this.updateToken(headerToken);
|
this.updateToken(headerToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (this.options.localStorage) {
|
} else if (this.storage) {
|
||||||
const token = localStorage.getItem(this.tokenKey);
|
this.storage.getItem(this.tokenKey).then((token) => {
|
||||||
if (token) {
|
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(token);
|
this.updateToken(token ? String(token) : undefined);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
private get storage() {
|
||||||
|
if (!this.options.storage) return null;
|
||||||
|
return {
|
||||||
|
getItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.getItem(key);
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.verified = false;
|
this.verified = false;
|
||||||
|
|
||||||
@@ -134,17 +159,25 @@ export class Api {
|
|||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.localStorage) {
|
if (this.storage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem(key, token);
|
this.storage.setItem(key, token).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(key);
|
this.storage.removeItem(key).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (opts?.trigger !== false) {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rebuild) this.buildApis();
|
if (opts?.rebuild) this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
private markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
@@ -214,15 +247,32 @@ export class Api {
|
|||||||
const fetcher = this.options.fetcher;
|
const fetcher = this.options.fetcher;
|
||||||
|
|
||||||
this.system = new SystemApi(baseParams, fetcher);
|
this.system = new SystemApi(baseParams, fetcher);
|
||||||
this.data = new DataApi(baseParams, fetcher);
|
this.data = new DataApi(
|
||||||
this.auth = new AuthApi(
|
|
||||||
{
|
{
|
||||||
...baseParams,
|
...baseParams,
|
||||||
onTokenUpdate: (token) => this.updateToken(token, true),
|
...this.options.data,
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.auth = new AuthApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
credentials: this.options.storage ? "omit" : "include",
|
||||||
|
...this.options.auth,
|
||||||
|
onTokenUpdate: (token) => {
|
||||||
|
this.updateToken(token, { rebuild: true });
|
||||||
|
this.options.auth?.onTokenUpdate?.(token);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.media = new MediaApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
...this.options.media,
|
||||||
},
|
},
|
||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
this.media = new MediaApi(baseParams, fetcher);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authent
|
|||||||
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
||||||
|
|
||||||
export type AuthApiOptions = BaseModuleApiOptions & {
|
export type AuthApiOptions = BaseModuleApiOptions & {
|
||||||
onTokenUpdate?: (token: string) => void | Promise<void>;
|
onTokenUpdate?: (token?: string) => void | Promise<void>;
|
||||||
|
credentials?: "include" | "same-origin" | "omit";
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AuthApi extends ModuleApi<AuthApiOptions> {
|
export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||||
protected override getDefaultOptions(): Partial<AuthApiOptions> {
|
protected override getDefaultOptions(): Partial<AuthApiOptions> {
|
||||||
return {
|
return {
|
||||||
basepath: "/api/auth",
|
basepath: "/api/auth",
|
||||||
|
credentials: "include",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(strategy: string, input: any) {
|
async login(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
||||||
credentials: "include",
|
credentials: this.options.credentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
@@ -27,7 +29,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async register(strategy: string, input: any) {
|
async register(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
||||||
credentials: "include",
|
credentials: this.options.credentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
@@ -68,5 +70,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {}
|
async logout() {
|
||||||
|
await this.options.onTokenUpdate?.(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class AdminController extends Controller {
|
|||||||
hono.use("*", async (c, next) => {
|
hono.use("*", async (c, next) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
user: c.get("auth")?.user,
|
user: c.get("auth")?.user,
|
||||||
logout_route: this.withAdminBasePath(authRoutes.logout),
|
logout_route: authRoutes.logout,
|
||||||
admin_basepath: this.options.adminBasepath,
|
admin_basepath: this.options.adminBasepath,
|
||||||
};
|
};
|
||||||
const html = await this.getHtml(obj);
|
const html = await this.getHtml(obj);
|
||||||
|
|||||||
@@ -1,52 +1,64 @@
|
|||||||
import { Api, type ApiOptions, type TApiUser } from "Api";
|
import { Api, type ApiOptions, type AuthState } from "Api";
|
||||||
import { isDebug } from "core";
|
import { isDebug } from "core";
|
||||||
import { createContext, type ReactNode, useContext } from "react";
|
import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
|
||||||
import type { AdminBkndWindowContext } from "modules/server/AdminController";
|
import type { AdminBkndWindowContext } from "modules/server/AdminController";
|
||||||
|
|
||||||
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
export type BkndClientContext = {
|
||||||
baseUrl: undefined,
|
baseUrl: string;
|
||||||
} as any);
|
api: Api;
|
||||||
|
authState?: Partial<AuthState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientContext = createContext<BkndClientContext>(undefined!);
|
||||||
|
|
||||||
export type ClientProviderProps = {
|
export type ClientProviderProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
} & (
|
baseUrl?: string;
|
||||||
| { baseUrl?: string; user?: TApiUser | null | undefined }
|
} & ApiOptions;
|
||||||
| {
|
|
||||||
api: Api;
|
export const ClientProvider = ({
|
||||||
|
children,
|
||||||
|
host,
|
||||||
|
baseUrl: _baseUrl = host,
|
||||||
|
...props
|
||||||
|
}: ClientProviderProps) => {
|
||||||
|
const winCtx = useBkndWindowContext();
|
||||||
|
const _ctx = useClientContext();
|
||||||
|
let actualBaseUrl = _baseUrl ?? _ctx?.baseUrl ?? "";
|
||||||
|
let user: any = undefined;
|
||||||
|
|
||||||
|
if (winCtx) {
|
||||||
|
user = winCtx.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!actualBaseUrl) {
|
||||||
|
try {
|
||||||
|
actualBaseUrl = window.location.origin;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiProps = { user, ...props, host: actualBaseUrl };
|
||||||
|
const api = useMemo(
|
||||||
|
() =>
|
||||||
|
new Api({
|
||||||
|
...apiProps,
|
||||||
|
verbose: isDebug(),
|
||||||
|
onAuthStateChange: (state) => {
|
||||||
|
props.onAuthStateChange?.(state);
|
||||||
|
if (!authState?.token || state.token !== authState?.token) {
|
||||||
|
setAuthState(state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[JSON.stringify(apiProps)],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ClientProvider = ({ children, ...props }: ClientProviderProps) => {
|
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(
|
||||||
let api: Api;
|
apiProps.user ? api.getAuthState() : undefined,
|
||||||
|
);
|
||||||
if (props && "api" in props) {
|
|
||||||
api = props.api;
|
|
||||||
} else {
|
|
||||||
const winCtx = useBkndWindowContext();
|
|
||||||
const _ctx_baseUrl = useBaseUrl();
|
|
||||||
const { baseUrl, user } = props;
|
|
||||||
let actualBaseUrl = baseUrl ?? _ctx_baseUrl ?? "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!baseUrl) {
|
|
||||||
if (_ctx_baseUrl) {
|
|
||||||
actualBaseUrl = _ctx_baseUrl;
|
|
||||||
console.warn("wrapped many times, take from context", actualBaseUrl);
|
|
||||||
} else if (typeof window !== "undefined") {
|
|
||||||
actualBaseUrl = window.location.origin;
|
|
||||||
//console.log("setting from window", actualBaseUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error in ClientProvider", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
|
|
||||||
api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user, verbose: isDebug() });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api }}>
|
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api, authState }}>
|
||||||
{children}
|
{children}
|
||||||
</ClientContext.Provider>
|
</ClientContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -61,12 +73,16 @@ export const useApi = (host?: ApiOptions["host"]): Api => {
|
|||||||
return context.api;
|
return context.api;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useClientContext = () => {
|
||||||
|
return useContext(ClientContext);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use useApi().baseUrl instead
|
* @deprecated use useApi().baseUrl instead
|
||||||
*/
|
*/
|
||||||
export const useBaseUrl = () => {
|
export const useBaseUrl = () => {
|
||||||
const context = useContext(ClientContext);
|
const context = useClientContext();
|
||||||
return context.baseUrl;
|
return context?.baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useBkndWindowContext(): AdminBkndWindowContext {
|
export function useBkndWindowContext(): AdminBkndWindowContext {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AuthState } from "Api";
|
import type { AuthState } from "Api";
|
||||||
import type { AuthResponse } from "auth";
|
import type { AuthResponse } from "auth";
|
||||||
import { useState } from "react";
|
|
||||||
import { useApi, useInvalidate } from "ui/client";
|
import { useApi, useInvalidate } from "ui/client";
|
||||||
|
import { useClientContext } from "ui/client/ClientProvider";
|
||||||
|
|
||||||
type LoginData = {
|
type LoginData = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -10,7 +10,7 @@ type LoginData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseAuth = {
|
type UseAuth = {
|
||||||
data: AuthState | undefined;
|
data: Partial<AuthState> | undefined;
|
||||||
user: AuthState["user"] | undefined;
|
user: AuthState["user"] | undefined;
|
||||||
token: AuthState["token"] | undefined;
|
token: AuthState["token"] | undefined;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
@@ -24,46 +24,36 @@ type UseAuth = {
|
|||||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||||
const api = useApi(options?.baseUrl);
|
const api = useApi(options?.baseUrl);
|
||||||
const invalidate = useInvalidate();
|
const invalidate = useInvalidate();
|
||||||
const authState = api.getAuthState();
|
const { authState } = useClientContext();
|
||||||
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
|
||||||
const verified = authState?.verified ?? false;
|
const verified = authState?.verified ?? false;
|
||||||
|
|
||||||
function updateAuthState() {
|
|
||||||
setAuthData(api.getAuthState());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login(input: LoginData) {
|
async function login(input: LoginData) {
|
||||||
const res = await api.auth.loginWithPassword(input);
|
const res = await api.auth.login("password", input);
|
||||||
updateAuthState();
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(input: LoginData) {
|
async function register(input: LoginData) {
|
||||||
const res = await api.auth.registerWithPassword(input);
|
const res = await api.auth.register("password", input);
|
||||||
updateAuthState();
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToken(token: string) {
|
function setToken(token: string) {
|
||||||
api.updateToken(token);
|
api.updateToken(token);
|
||||||
updateAuthState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await api.updateToken(undefined);
|
api.updateToken(undefined);
|
||||||
setAuthData(undefined);
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
async function verify() {
|
||||||
await api.verifyAuth();
|
await api.verifyAuth();
|
||||||
updateAuthState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: authData,
|
data: authState,
|
||||||
user: authData?.user,
|
user: authState?.user,
|
||||||
token: authData?.token,
|
token: authState?.token,
|
||||||
verified,
|
verified,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import {
|
||||||
|
default as CodeMirror,
|
||||||
|
type ReactCodeMirrorProps,
|
||||||
|
EditorView,
|
||||||
|
} from "@uiw/react-codemirror";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { html } from "@codemirror/lang-html";
|
import { html } from "@codemirror/lang-html";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
@@ -43,7 +47,7 @@ export default function CodeEditor({
|
|||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
basicSetup={_basicSetup}
|
basicSetup={_basicSetup}
|
||||||
extensions={extensions}
|
extensions={[...extensions, EditorView.lineWrapping]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type DropdownItem =
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ export function Dropdown({
|
|||||||
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white",
|
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
title={item.title}
|
||||||
>
|
>
|
||||||
{space_for_icon && (
|
{space_for_icon && (
|
||||||
<div className="size-[16px] text-left mr-1.5 opacity-80">
|
<div className="size-[16px] text-left mr-1.5 opacity-80">
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ function UserMenu() {
|
|||||||
items.push({ label: "Login", onClick: handleLogin, icon: IconUser });
|
items.push({ label: "Login", onClick: handleLogin, icon: IconUser });
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
label: `Logout ${auth.user.email}`,
|
label: "Logout",
|
||||||
|
title: `Logout ${auth.user.email}`,
|
||||||
onClick: handleLogout,
|
onClick: handleLogout,
|
||||||
icon: IconKeyOff,
|
icon: IconKeyOff,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
|||||||
.cm-editor {
|
.cm-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
|
|||||||
Reference in New Issue
Block a user