mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(console): polish code by improving data types and API routes (#3324)
This commit is contained in:
parent
01735d1647
commit
1f0bc8f3f4
11 changed files with 43 additions and 39 deletions
|
@ -6,7 +6,12 @@ import FallbackImageDark from '@/assets/images/broken-image-dark.svg';
|
|||
import FallbackImageLight from '@/assets/images/broken-image-light.svg';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
const ImageWithErrorFallback = ({ src, alt, className }: ImgHTMLAttributes<HTMLImageElement>) => {
|
||||
const ImageWithErrorFallback = ({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
...props
|
||||
}: ImgHTMLAttributes<HTMLImageElement>) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const theme = useTheme();
|
||||
const Fallback = theme === AppearanceMode.LightMode ? FallbackImageLight : FallbackImageDark;
|
||||
|
@ -19,7 +24,7 @@ const ImageWithErrorFallback = ({ src, alt, className }: ImgHTMLAttributes<HTMLI
|
|||
return <Fallback className={className} />;
|
||||
}
|
||||
|
||||
return <img className={className} src={src} alt={alt} onError={errorHandler} />;
|
||||
return <img className={className} src={src} alt={alt} onError={errorHandler} {...props} />;
|
||||
};
|
||||
|
||||
export default ImageWithErrorFallback;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import type { UserProfileResponse } from '@logto/schemas';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { adminTenantEndpoint, meApi } from '@/consts';
|
||||
|
@ -11,12 +11,12 @@ import useSwrFetcher from './use-swr-fetcher';
|
|||
const useCurrentUser = () => {
|
||||
const userId = useLogtoUserId();
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
|
||||
const fetcher = useSwrFetcher<User>(api);
|
||||
const fetcher = useSwrFetcher<UserProfileResponse>(api);
|
||||
const {
|
||||
data: user,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<User, RequestError>(userId && `me/users/${userId}`, fetcher);
|
||||
} = useSWR<UserProfileResponse, RequestError>(userId && 'me', fetcher);
|
||||
|
||||
const isLoading = !user && !error;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import type { UserInfo } from '@logto/schemas';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import type { Row } from '../CardContent';
|
|||
import CardContent from '../CardContent';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
user: UserInfo;
|
||||
onUpdate?: () => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ type Props<T> = {
|
|||
data: Array<Row<T>>;
|
||||
};
|
||||
|
||||
const CardContent = <T extends Nullable<string | Record<string, unknown>> | undefined>({
|
||||
const CardContent = <T extends Nullable<boolean | string | Record<string, unknown>> | undefined>({
|
||||
title,
|
||||
data,
|
||||
}: Props<T>) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { buildIdGenerator } from '@logto/core-kit';
|
||||
import type { ConnectorResponse, User } from '@logto/schemas';
|
||||
import type { ConnectorResponse, UserInfo } from '@logto/schemas';
|
||||
import { AppearanceMode } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { appendPath, conditional } from '@silverhand/essentials';
|
||||
|
@ -27,7 +27,7 @@ import NotSet from '../NotSet';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
user: UserInfo;
|
||||
connectors?: ConnectorResponse[];
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ const LinkAccountSection = ({ user, connectors, onUpdate }: Props) => {
|
|||
|
||||
return connectors.map(({ id, name, logo, logoDark, target }) => {
|
||||
const logoSrc = theme === AppearanceMode.DarkMode && logoDark ? logoDark : logo;
|
||||
const relatedUserDetails = user.identities[target]?.details;
|
||||
const relatedUserDetails = user.identities?.[target]?.details;
|
||||
const hasLinked = is(relatedUserDetails, socialUserInfoGuard);
|
||||
const conditionalUnlinkAction: Action[] = hasLinked
|
||||
? [
|
||||
|
|
|
@ -72,7 +72,7 @@ const BasicUserInfoUpdateModal = ({ field, value: initialValue, isOpen, onClose
|
|||
const onSubmit = async () => {
|
||||
clearErrors();
|
||||
void handleSubmit(async (data) => {
|
||||
await api.patch(`me/user`, { json: { [field]: data[field] } });
|
||||
await api.patch('me', { json: { [field]: data[field] } });
|
||||
toast.success(t('profile.updated', { target: t(`profile.settings.${field}`) }));
|
||||
onClose();
|
||||
})();
|
||||
|
|
|
@ -64,7 +64,7 @@ const VerificationCodeModal = () => {
|
|||
await api.post(`me/verification-codes/verify`, { json: { verificationCode, email } });
|
||||
|
||||
if (action === 'changeEmail') {
|
||||
await api.patch(`me/user`, { json: { primaryEmail: email } });
|
||||
await api.patch('me', { json: { primaryEmail: email } });
|
||||
toast.success(t('profile.email_changed'));
|
||||
|
||||
onClose();
|
||||
|
|
|
@ -61,12 +61,12 @@ const Profile = () => {
|
|||
{
|
||||
key: 'password',
|
||||
label: 'profile.password.password',
|
||||
value: user.passwordEncrypted,
|
||||
value: user.hasPassword,
|
||||
renderer: (value) => (value ? <span>********</span> : <NotSet />),
|
||||
action: {
|
||||
name: 'profile.change',
|
||||
handler: () => {
|
||||
navigate(user.passwordEncrypted ? 'verify-password' : 'change-password', {
|
||||
navigate(user.hasPassword ? 'verify-password' : 'change-password', {
|
||||
state: { email: user.primaryEmail, action: 'changePassword' },
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import type { UserProfileResponse } from '@logto/schemas';
|
||||
import { userInfoSelectFields, arbitraryObjectGuard } from '@logto/schemas';
|
||||
import { pick } from '@silverhand/essentials';
|
||||
import { conditional, pick } from '@silverhand/essentials';
|
||||
import { literal, object, string } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
@ -25,26 +26,23 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
},
|
||||
} = tenant;
|
||||
|
||||
router.get(
|
||||
'/users/:userId',
|
||||
koaGuard({
|
||||
params: object({ userId: string() }),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { userId },
|
||||
} = ctx.guard;
|
||||
router.get('/', async (ctx, next) => {
|
||||
const { id: userId } = ctx.auth;
|
||||
|
||||
const user = await findUserById(userId);
|
||||
const user = await findUserById(userId);
|
||||
|
||||
ctx.body = pick(user, ...userInfoSelectFields, 'passwordEncrypted');
|
||||
const responseData: UserProfileResponse = {
|
||||
...pick(user, ...userInfoSelectFields),
|
||||
...conditional(user.passwordEncrypted && { hasPassword: Boolean(user.passwordEncrypted) }),
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
ctx.body = responseData;
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.patch(
|
||||
'/user',
|
||||
'/',
|
||||
koaGuard({
|
||||
body: object({
|
||||
username: string().regex(usernameRegEx),
|
||||
|
@ -61,8 +59,9 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
await checkIdentifierCollision(body, userId);
|
||||
await updateUserById(userId, body);
|
||||
ctx.status = 204;
|
||||
|
||||
const updatedUser = await updateUserById(userId, body);
|
||||
ctx.body = pick(updatedUser, ...userInfoSelectFields);
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -71,6 +70,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
router.get('/custom-data', async (ctx, next) => {
|
||||
const { id: userId } = ctx.auth;
|
||||
const user = await findUserById(userId);
|
||||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
ctx.body = user.customData;
|
||||
|
||||
|
@ -87,13 +87,14 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
const { id: userId } = ctx.auth;
|
||||
const { body: customData } = ctx.guard;
|
||||
|
||||
await findUserById(userId);
|
||||
const user = await findUserById(userId);
|
||||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
const user = await updateUserById(userId, {
|
||||
const updatedUser = await updateUserById(userId, {
|
||||
customData,
|
||||
});
|
||||
|
||||
ctx.body = user.customData;
|
||||
ctx.body = updatedUser.customData;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
|
|||
'/verification-codes',
|
||||
koaGuard({
|
||||
body: object({ email: string().regex(emailRegEx) }),
|
||||
status: 204,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const code = await createPasscode(undefined, codeType, ctx.guard.body);
|
||||
|
@ -48,7 +47,6 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
|
|||
verificationCode: string().min(1),
|
||||
action: union([literal('changeEmail'), literal('changePassword')]),
|
||||
}),
|
||||
status: 204,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId } = ctx.auth;
|
||||
|
|
|
@ -20,7 +20,7 @@ export type UserInfo<Keys extends keyof CreateUser = (typeof userInfoSelectField
|
|||
Keys
|
||||
>;
|
||||
|
||||
export type UserProfileResponse = UserInfo & { hasPasswordSet: boolean };
|
||||
export type UserProfileResponse = UserInfo & { hasPassword?: boolean };
|
||||
|
||||
export enum UserRole {
|
||||
Admin = 'admin',
|
||||
|
|
Loading…
Reference in a new issue