0
Fork 0
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:
Charles Zhao 2023-03-08 16:28:33 +08:00 committed by GitHub
parent 01735d1647
commit 1f0bc8f3f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 43 additions and 39 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;
};

View file

@ -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>) => {

View file

@ -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
? [

View file

@ -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();
})();

View file

@ -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();

View file

@ -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' },
});
},

View file

@ -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();
}

View file

@ -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;

View file

@ -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',