0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(console): send access token in requests (#386)

* feat(console): send access token in requests

* fix(console): upgrade SDK and fulfill resources

* fix(console): build
This commit is contained in:
Gao Sun 2022-03-16 15:34:03 +08:00 committed by GitHub
parent b288e8eb6c
commit 1bc9568083
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 188 additions and 123 deletions

View file

@ -17,7 +17,7 @@
},
"dependencies": {
"@logto/phrases": "^0.1.0",
"@logto/react": "^0.1.2",
"@logto/react": "^0.1.3",
"@logto/schemas": "^0.1.0",
"@monaco-editor/react": "^4.3.1",
"@silverhand/essentials": "^1.1.6",

View file

@ -7,6 +7,8 @@ import './scss/normalized.scss';
import AppContent from './components/AppContent';
import { getPath, sections } from './components/AppContent/components/Sidebar';
import Toast from './components/Toast';
import { logtoApiResource } from './consts/api';
import useSwrFetcher from './hooks/use-swr-fetcher';
import initI18n from './i18n/init';
import ApiResourceDetails from './pages/ApiResourceDetails';
import ApiResources from './pages/ApiResources';
@ -18,7 +20,6 @@ import Connectors from './pages/Connectors';
import GetStarted from './pages/GetStarted';
import UserDetails from './pages/UserDetails';
import Users from './pages/Users';
import { fetcher } from './swr';
const isBasenameNeeded = process.env.NODE_ENV !== 'development' || process.env.PORT === '5002';
@ -27,6 +28,7 @@ void initI18n();
const Main = () => {
const location = useLocation();
const navigate = useNavigate();
const fetcher = useSwrFetcher();
useEffect(() => {
if (location.pathname === '/') {
@ -35,44 +37,46 @@ const Main = () => {
}, [location.pathname, navigate]);
return (
<LogtoProvider logtoConfig={{ endpoint: window.location.origin, clientId: 'foo' }}>
<SWRConfig value={{ fetcher }}>
<Toast />
<Routes>
<Route path="callback" element={<Callback />} />
<Route element={<AppContent theme="light" />}>
<Route path="get-started" element={<GetStarted />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path=":id">
<Route index element={<Navigate to="settings" />} />
<Route path="settings" element={<ApplicationDetails />} />
<Route path="advanced-settings" element={<ApplicationDetails />} />
</Route>
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />} />
</Route>
<Route path="connectors">
<Route index element={<Connectors />} />
<Route path="social" element={<Connectors />} />
<Route path=":connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path=":id" element={<UserDetails />} />
<SWRConfig value={{ fetcher }}>
<Toast />
<Routes>
<Route path="callback" element={<Callback />} />
<Route element={<AppContent theme="light" />}>
<Route path="get-started" element={<GetStarted />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path=":id">
<Route index element={<Navigate to="settings" />} />
<Route path="settings" element={<ApplicationDetails />} />
<Route path="advanced-settings" element={<ApplicationDetails />} />
</Route>
</Route>
</Routes>
</SWRConfig>
</LogtoProvider>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />} />
</Route>
<Route path="connectors">
<Route index element={<Connectors />} />
<Route path="social" element={<Connectors />} />
<Route path=":connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path=":id" element={<UserDetails />} />
</Route>
</Route>
</Routes>
</SWRConfig>
);
};
const App = () => (
<BrowserRouter basename={isBasenameNeeded ? '/console' : ''}>
<Main />
<LogtoProvider
config={{ endpoint: window.location.origin, clientId: 'foo', resources: [logtoApiResource] }}
>
<Main />
</LogtoProvider>
</BrowserRouter>
);

View file

@ -0,0 +1 @@
export const logtoApiResource = 'https://api.logto.io';

View file

@ -0,0 +1,7 @@
import { ApplicationType } from '@logto/schemas';
export const applicationTypeI18nKey = Object.freeze({
[ApplicationType.Native]: 'applications.type.native',
[ApplicationType.SPA]: 'applications.type.spa',
[ApplicationType.Traditional]: 'applications.type.traditional',
} as const);

View file

@ -0,0 +1,60 @@
import { useLogto } from '@logto/react';
import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas';
import { t } from 'i18next';
import ky from 'ky';
import { useMemo } from 'react';
import { toast } from 'react-hot-toast';
import { logtoApiResource } from '@/consts/api';
export class RequestError extends Error {
metadata: RequestErrorMetadata;
constructor(metadata: RequestErrorMetadata) {
super('Request error occurred.');
this.metadata = metadata;
}
}
const toastError = async (response: Response) => {
try {
const data = (await response.json()) as RequestErrorBody;
toast.error(data.message || t('admin_console.errors.unknown_server_error'));
} catch {
toast.error(t('admin_console.errors.unknown_server_error'));
}
};
const useApi = () => {
const { isAuthenticated, getAccessToken } = useLogto();
const api = useMemo(
() =>
ky.create({
hooks: {
beforeError: [
(error) => {
const { response } = error;
void toastError(response);
return error;
},
],
beforeRequest: [
async (request) => {
if (isAuthenticated) {
const accessToken = await getAccessToken(logtoApiResource);
request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`);
}
},
],
},
}),
[getAccessToken, isAuthenticated]
);
return api;
};
export default useApi;

View file

@ -0,0 +1,26 @@
import { RequestErrorMetadata } from '@logto/schemas';
import { useCallback } from 'react';
import { BareFetcher } from 'swr';
import useApi, { RequestError } from './use-api';
const useSwrFetcher = () => {
const api = useApi();
const fetcher = useCallback<BareFetcher>(
async (resource, init) => {
const response = await api.get(resource, init);
if (!response.ok) {
const metadata = (await response.json()) as RequestErrorMetadata;
throw new RequestError(metadata);
}
return response.json();
},
[api]
);
return fetcher;
};
export default useSwrFetcher;

View file

@ -7,8 +7,8 @@ import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import api from '@/utilities/api';
import * as styles from './index.module.scss';
@ -19,6 +19,7 @@ type Props = {
};
const DeleteForm = ({ id, name, onClose }: Props) => {
const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const navigate = useNavigate();

View file

@ -15,9 +15,8 @@ import FormField from '@/components/FormField';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import TabNav, { TabNavLink } from '@/components/TabNav';
import TextInput from '@/components/TextInput';
import useApi, { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { RequestError } from '@/swr';
import api from '@/utilities/api';
import DeleteForm from './components/DeleteForm';
import * as styles from './index.module.scss';
@ -39,6 +38,7 @@ const ApiResourceDetails = () => {
defaultValues: data,
});
const [submitting, setSubmitting] = useState(false);
const api = useApi();
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);

View file

@ -7,8 +7,8 @@ import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import api from '@/utilities/api';
import * as styles from './index.module.scss';
@ -23,6 +23,7 @@ type Props = {
const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>();
const api = useApi();
const onSubmit = handleSubmit(async (data) => {
try {

View file

@ -12,8 +12,8 @@ import CardTitle from '@/components/CardTitle';
import CopyToClipboard from '@/components/CopyToClipboard';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import ItemPreview from '@/components/ItemPreview';
import { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { RequestError } from '@/swr';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';

View file

@ -13,7 +13,7 @@ import FormField from '@/components/FormField';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import TabNav, { TabNavLink } from '@/components/TabNav';
import TextInput from '@/components/TextInput';
import { RequestError } from '@/swr';
import { RequestError } from '@/hooks/use-api';
import { applicationTypeI18nKey } from '@/types/applications';
import * as styles from './index.module.scss';

View file

@ -9,9 +9,9 @@ import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import { applicationTypeI18nKey } from '@/types/applications';
import api from '@/utilities/api';
import TypeDescription from '../TypeDescription';
import * as styles from './index.module.scss';
@ -37,6 +37,7 @@ const CreateForm = ({ onClose }: Props) => {
field: { onChange, value, name, ref },
} = useController({ name: 'type', control, rules: { required: true } });
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi();
const onSubmit = handleSubmit(async (data) => {
try {

View file

@ -13,8 +13,8 @@ import CardTitle from '@/components/CardTitle';
import CopyToClipboard from '@/components/CopyToClipboard';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import ItemPreview from '@/components/ItemPreview';
import { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { RequestError } from '@/swr';
import { applicationTypeI18nKey } from '@/types/applications';
import CreateForm from './components/CreateForm';

View file

@ -1,4 +1,4 @@
import { useLogto } from '@logto/react';
import { useHandleSignInCallback, useLogto } from '@logto/react';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@ -6,6 +6,8 @@ const Callback = () => {
const { isAuthenticated, isLoading } = useLogto();
const navigate = useNavigate();
useHandleSignInCallback();
// TO-DO: Error handling
useEffect(() => {
if (isAuthenticated && !isLoading) {

View file

@ -15,8 +15,7 @@ import ImagePlaceholder from '@/components/ImagePlaceholder';
import Markdown from '@/components/Markdown';
import Status from '@/components/Status';
import TabNav, { TabNavLink } from '@/components/TabNav';
import { RequestError } from '@/swr';
import api from '@/utilities/api';
import useApi, { RequestError } from '@/hooks/use-api';
import SenderTester from './components/SenderTester';
import * as styles from './index.module.scss';
@ -35,6 +34,7 @@ const ConnectorDetails = () => {
connectorId && `/api/connectors/${connectorId}`
);
const isLoading = !data && !error;
const api = useApi();
useEffect(() => {
if (data) {

View file

@ -9,7 +9,7 @@ import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import TabNav, { TabNavLink } from '@/components/TabNav';
import { RequestError } from '@/swr';
import { RequestError } from '@/hooks/use-api';
import ConnectorRow from './components/ConnectorRow';
import * as styles from './index.module.scss';

View file

@ -8,7 +8,7 @@ import BackLink from '@/components/BackLink';
import Card from '@/components/Card';
import CopyToClipboard from '@/components/CopyToClipboard';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import { RequestError } from '@/swr';
import { RequestError } from '@/hooks/use-api';
import CreateSuccess from './components/CreateSuccess';
import * as styles from './index.module.scss';

View file

@ -7,8 +7,8 @@ import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import api from '@/utilities/api';
import * as styles from './index.module.scss';
@ -24,6 +24,7 @@ type Props = {
const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>();
const api = useApi();
const onSubmit = handleSubmit(async (data) => {
const createdUser = await api.post('/api/users', { json: data }).json<User>();

View file

@ -10,8 +10,8 @@ import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import ItemPreview from '@/components/ItemPreview';
import { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { RequestError } from '@/swr';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';

View file

@ -1,22 +0,0 @@
import { RequestErrorMetadata } from '@logto/schemas';
import { BareFetcher } from 'swr';
export class RequestError extends Error {
metadata: RequestErrorMetadata;
constructor(metadata: RequestErrorMetadata) {
super('Request error occurred.');
this.metadata = metadata;
}
}
export const fetcher: BareFetcher = async (resource, init) => {
const response = await fetch(resource, init);
if (!response.ok) {
const metadata = (await response.json()) as RequestErrorMetadata;
throw new RequestError(metadata);
}
return response.json();
};

View file

@ -1,29 +0,0 @@
import { RequestErrorBody } from '@logto/schemas';
import { t } from 'i18next';
import ky from 'ky';
import { toast } from 'react-hot-toast';
const toastError = async (response: Response) => {
try {
const data = (await response.json()) as RequestErrorBody;
toast.error(data.message || t('admin_console.errors.unknown_server_error'));
} catch {
toast.error(t('admin_console.errors.unknown_server_error'));
}
};
const api = ky.create({
hooks: {
beforeError: [
(error) => {
const { response } = error;
void toastError(response);
return error;
},
],
},
});
export default api;

60
pnpm-lock.yaml generated
View file

@ -21,7 +21,7 @@ importers:
packages/console:
specifiers:
'@logto/phrases': ^0.1.0
'@logto/react': ^0.1.2
'@logto/react': ^0.1.3
'@logto/schemas': ^0.1.0
'@monaco-editor/react': ^4.3.1
'@parcel/core': ^2.3.2
@ -63,7 +63,7 @@ importers:
typescript: ^4.6.2
dependencies:
'@logto/phrases': link:../phrases
'@logto/react': 0.1.2_react@17.0.2
'@logto/react': 0.1.3_react@17.0.2
'@logto/schemas': link:../schemas
'@monaco-editor/react': 4.3.1_e62f1489d5efe674a41c3f8d6971effe
'@silverhand/essentials': 1.1.6
@ -2222,19 +2222,19 @@ packages:
write-file-atomic: 3.0.3
dev: true
/@logto/browser/0.1.2:
resolution: {integrity: sha512-sTJjnx00BXYEChCbbO/LPs0x0wE1bDSHniFi+u93cynyEHgoT5yjMnH4N39NhrpmRdkXxOxaIkXmyAT1nSmYzQ==}
/@logto/browser/0.1.3:
resolution: {integrity: sha512-SsXp7uWnDKSyYGbcCCUkTelcWs8OlUNO0gVnW1hvuAAgm62LAX6EfgsE4EIKCLN/JrcRfjbiJIr98QNfa0riWw==}
requiresBuild: true
dependencies:
'@logto/js': 0.1.2
'@logto/js': 0.1.3
'@silverhand/essentials': 1.1.6
jose: 4.6.0
lodash.get: 4.4.2
superstruct: 0.15.4
dev: false
/@logto/js/0.1.2:
resolution: {integrity: sha512-kz7l++gfpXa1OaUaB5WvnHNXQKEJDFnBzVYAofPADnPftHEF0zYRuQDCa8PJMd6/ECKfL1XPLMlJMNP/5U2fqQ==}
/@logto/js/0.1.3:
resolution: {integrity: sha512-lxdZNX67w2rntiSHC5ovY2D7IHq8ldcT9AQDaP7ayjKTyIuWIJxW7u9f21BpuPpfBHAIhOsKjgqAvLPBXczjzw==}
requiresBuild: true
dependencies:
'@silverhand/essentials': 1.1.6
@ -2245,13 +2245,13 @@ packages:
superstruct: 0.15.4
dev: false
/@logto/react/0.1.2_react@17.0.2:
resolution: {integrity: sha512-kxLOvxOv3IB8BjpbcoRFsBFX249wAfbOL0tfJlSWmOXrkPouai1bRItEFBHFKDI1zLJATVQRoJNQfaczJy/c0A==}
/@logto/react/0.1.3_react@17.0.2:
resolution: {integrity: sha512-cxa+LM+sb0azQY5tGe5smY4B5N6U/yE66GT6RL0CguHC6DXZ3j00wfzaRhwAtOXAl3QL2xULcZTwkI3sY69COw==}
requiresBuild: true
peerDependencies:
react: '>=16.8.0'
dependencies:
'@logto/browser': 0.1.2
'@logto/browser': 0.1.3
'@silverhand/essentials': 1.1.6
react: 17.0.2
dev: false
@ -2366,8 +2366,8 @@ packages:
'@octokit/types': 6.34.0
dev: true
/@octokit/core/3.5.1:
resolution: {integrity: sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==}
/@octokit/core/3.6.0:
resolution: {integrity: sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==}
dependencies:
'@octokit/auth-token': 2.5.0
'@octokit/graphql': 4.8.0
@ -2406,29 +2406,29 @@ packages:
resolution: {integrity: sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==}
dev: true
/@octokit/plugin-paginate-rest/2.17.0_@octokit+core@3.5.1:
/@octokit/plugin-paginate-rest/2.17.0_@octokit+core@3.6.0:
resolution: {integrity: sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==}
peerDependencies:
'@octokit/core': '>=2'
dependencies:
'@octokit/core': 3.5.1
'@octokit/core': 3.6.0
'@octokit/types': 6.34.0
dev: true
/@octokit/plugin-request-log/1.0.4_@octokit+core@3.5.1:
/@octokit/plugin-request-log/1.0.4_@octokit+core@3.6.0:
resolution: {integrity: sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==}
peerDependencies:
'@octokit/core': '>=3'
dependencies:
'@octokit/core': 3.5.1
'@octokit/core': 3.6.0
dev: true
/@octokit/plugin-rest-endpoint-methods/5.13.0_@octokit+core@3.5.1:
/@octokit/plugin-rest-endpoint-methods/5.13.0_@octokit+core@3.6.0:
resolution: {integrity: sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==}
peerDependencies:
'@octokit/core': '>=3'
dependencies:
'@octokit/core': 3.5.1
'@octokit/core': 3.6.0
'@octokit/types': 6.34.0
deprecation: 2.3.1
dev: true
@ -2457,10 +2457,10 @@ packages:
/@octokit/rest/18.12.0:
resolution: {integrity: sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==}
dependencies:
'@octokit/core': 3.5.1
'@octokit/plugin-paginate-rest': 2.17.0_@octokit+core@3.5.1
'@octokit/plugin-request-log': 1.0.4_@octokit+core@3.5.1
'@octokit/plugin-rest-endpoint-methods': 5.13.0_@octokit+core@3.5.1
'@octokit/core': 3.6.0
'@octokit/plugin-paginate-rest': 2.17.0_@octokit+core@3.6.0
'@octokit/plugin-request-log': 1.0.4_@octokit+core@3.6.0
'@octokit/plugin-rest-endpoint-methods': 5.13.0_@octokit+core@3.6.0
transitivePeerDependencies:
- encoding
dev: true
@ -6377,7 +6377,7 @@ packages:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.34
mime-types: 2.1.35
dev: true
/form-data/3.0.1:
@ -9850,12 +9850,24 @@ packages:
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
engines: {node: '>= 0.6'}
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: true
/mime-types/2.1.34:
resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.51.0
/mime-types/2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: true
/mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
@ -12263,7 +12275,7 @@ packages:
is-typedarray: 1.0.0
isstream: 0.1.2
json-stringify-safe: 5.0.1
mime-types: 2.1.34
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.3