0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-13 21:30:30 -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 FallbackImageLight from '@/assets/images/broken-image-light.svg';
import { useTheme } from '@/hooks/use-theme'; 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 [hasError, setHasError] = useState(false);
const theme = useTheme(); const theme = useTheme();
const Fallback = theme === AppearanceMode.LightMode ? FallbackImageLight : FallbackImageDark; const Fallback = theme === AppearanceMode.LightMode ? FallbackImageLight : FallbackImageDark;
@ -19,7 +24,7 @@ const ImageWithErrorFallback = ({ src, alt, className }: ImgHTMLAttributes<HTMLI
return <Fallback className={className} />; 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; 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 useSWR from 'swr';
import { adminTenantEndpoint, meApi } from '@/consts'; import { adminTenantEndpoint, meApi } from '@/consts';
@ -11,12 +11,12 @@ import useSwrFetcher from './use-swr-fetcher';
const useCurrentUser = () => { const useCurrentUser = () => {
const userId = useLogtoUserId(); const userId = useLogtoUserId();
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator }); const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
const fetcher = useSwrFetcher<User>(api); const fetcher = useSwrFetcher<UserProfileResponse>(api);
const { const {
data: user, data: user,
error, error,
mutate, mutate,
} = useSWR<User, RequestError>(userId && `me/users/${userId}`, fetcher); } = useSWR<UserProfileResponse, RequestError>(userId && 'me', fetcher);
const isLoading = !user && !error; 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 type { Nullable } from '@silverhand/essentials';
import { useState } from 'react'; import { useState } from 'react';
@ -12,7 +12,7 @@ import type { Row } from '../CardContent';
import CardContent from '../CardContent'; import CardContent from '../CardContent';
type Props = { type Props = {
user: User; user: UserInfo;
onUpdate?: () => void; onUpdate?: () => void;
}; };

View file

@ -28,7 +28,7 @@ type Props<T> = {
data: Array<Row<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, title,
data, data,
}: Props<T>) => { }: Props<T>) => {

View file

@ -1,5 +1,5 @@
import { buildIdGenerator } from '@logto/core-kit'; 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 { AppearanceMode } from '@logto/schemas';
import type { Optional } from '@silverhand/essentials'; import type { Optional } from '@silverhand/essentials';
import { appendPath, conditional } from '@silverhand/essentials'; import { appendPath, conditional } from '@silverhand/essentials';
@ -27,7 +27,7 @@ import NotSet from '../NotSet';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
user: User; user: UserInfo;
connectors?: ConnectorResponse[]; connectors?: ConnectorResponse[];
onUpdate: () => void; onUpdate: () => void;
}; };
@ -62,7 +62,7 @@ const LinkAccountSection = ({ user, connectors, onUpdate }: Props) => {
return connectors.map(({ id, name, logo, logoDark, target }) => { return connectors.map(({ id, name, logo, logoDark, target }) => {
const logoSrc = theme === AppearanceMode.DarkMode && logoDark ? logoDark : logo; 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 hasLinked = is(relatedUserDetails, socialUserInfoGuard);
const conditionalUnlinkAction: Action[] = hasLinked const conditionalUnlinkAction: Action[] = hasLinked
? [ ? [

View file

@ -72,7 +72,7 @@ const BasicUserInfoUpdateModal = ({ field, value: initialValue, isOpen, onClose
const onSubmit = async () => { const onSubmit = async () => {
clearErrors(); clearErrors();
void handleSubmit(async (data) => { 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}`) })); toast.success(t('profile.updated', { target: t(`profile.settings.${field}`) }));
onClose(); onClose();
})(); })();

View file

@ -64,7 +64,7 @@ const VerificationCodeModal = () => {
await api.post(`me/verification-codes/verify`, { json: { verificationCode, email } }); await api.post(`me/verification-codes/verify`, { json: { verificationCode, email } });
if (action === 'changeEmail') { if (action === 'changeEmail') {
await api.patch(`me/user`, { json: { primaryEmail: email } }); await api.patch('me', { json: { primaryEmail: email } });
toast.success(t('profile.email_changed')); toast.success(t('profile.email_changed'));
onClose(); onClose();

View file

@ -61,12 +61,12 @@ const Profile = () => {
{ {
key: 'password', key: 'password',
label: 'profile.password.password', label: 'profile.password.password',
value: user.passwordEncrypted, value: user.hasPassword,
renderer: (value) => (value ? <span>********</span> : <NotSet />), renderer: (value) => (value ? <span>********</span> : <NotSet />),
action: { action: {
name: 'profile.change', name: 'profile.change',
handler: () => { handler: () => {
navigate(user.passwordEncrypted ? 'verify-password' : 'change-password', { navigate(user.hasPassword ? 'verify-password' : 'change-password', {
state: { email: user.primaryEmail, action: 'changePassword' }, state: { email: user.primaryEmail, action: 'changePassword' },
}); });
}, },

View file

@ -1,6 +1,7 @@
import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit'; import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit';
import type { UserProfileResponse } from '@logto/schemas';
import { userInfoSelectFields, arbitraryObjectGuard } 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 { literal, object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -25,26 +26,23 @@ export default function userRoutes<T extends AuthedMeRouter>(
}, },
} = tenant; } = tenant;
router.get( router.get('/', async (ctx, next) => {
'/users/:userId', const { id: userId } = ctx.auth;
koaGuard({
params: object({ userId: string() }),
}),
async (ctx, next) => {
const {
params: { userId },
} = ctx.guard;
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) }),
};
ctx.body = responseData;
return next(); return next();
} });
);
router.patch( router.patch(
'/user', '/',
koaGuard({ koaGuard({
body: object({ body: object({
username: string().regex(usernameRegEx), 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 })); assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
await checkIdentifierCollision(body, userId); await checkIdentifierCollision(body, userId);
await updateUserById(userId, body);
ctx.status = 204; const updatedUser = await updateUserById(userId, body);
ctx.body = pick(updatedUser, ...userInfoSelectFields);
return next(); return next();
} }
@ -71,6 +70,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
router.get('/custom-data', async (ctx, next) => { router.get('/custom-data', async (ctx, next) => {
const { id: userId } = ctx.auth; const { id: userId } = ctx.auth;
const user = await findUserById(userId); const user = await findUserById(userId);
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
ctx.body = user.customData; ctx.body = user.customData;
@ -87,13 +87,14 @@ export default function userRoutes<T extends AuthedMeRouter>(
const { id: userId } = ctx.auth; const { id: userId } = ctx.auth;
const { body: customData } = ctx.guard; 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, customData,
}); });
ctx.body = user.customData; ctx.body = updatedUser.customData;
return next(); return next();
} }

View file

@ -28,7 +28,6 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
'/verification-codes', '/verification-codes',
koaGuard({ koaGuard({
body: object({ email: string().regex(emailRegEx) }), body: object({ email: string().regex(emailRegEx) }),
status: 204,
}), }),
async (ctx, next) => { async (ctx, next) => {
const code = await createPasscode(undefined, codeType, ctx.guard.body); const code = await createPasscode(undefined, codeType, ctx.guard.body);
@ -48,7 +47,6 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
verificationCode: string().min(1), verificationCode: string().min(1),
action: union([literal('changeEmail'), literal('changePassword')]), action: union([literal('changeEmail'), literal('changePassword')]),
}), }),
status: 204,
}), }),
async (ctx, next) => { async (ctx, next) => {
const { id: userId } = ctx.auth; const { id: userId } = ctx.auth;

View file

@ -20,7 +20,7 @@ export type UserInfo<Keys extends keyof CreateUser = (typeof userInfoSelectField
Keys Keys
>; >;
export type UserProfileResponse = UserInfo & { hasPasswordSet: boolean }; export type UserProfileResponse = UserInfo & { hasPassword?: boolean };
export enum UserRole { export enum UserRole {
Admin = 'admin', Admin = 'admin',