mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
Merge branch master
into merge/sie-v2
This commit is contained in:
commit
42de419f44
25 changed files with 253 additions and 149 deletions
13
.devcontainer/devcontainer.json
Normal file
13
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"updateContentCommand": "npm i -g pnpm && pnpm i && pnpm prepack && pnpm cli connector add --official -p .",
|
||||
"postStartCommand": "docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=p0stgr3s postgres:14-alpine",
|
||||
"postAttachCommand": "pnpm cli db seed && [[ ! -z $CODESPACES ]] && export ENDPOINT=https://$CODESPACE_NAME-3001.preview.app.github.dev",
|
||||
"containerEnv": {
|
||||
"DB_URL": "postgres://postgres:p0stgr3s@localhost:5432/logto",
|
||||
"TRUST_PROXY_HEADER": "1"
|
||||
}
|
||||
}
|
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
main-lint:
|
||||
# avoid out of memory issue since macOS has bigger memory
|
||||
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -29,6 +29,7 @@ cache
|
|||
.idea/
|
||||
*.pem
|
||||
.history
|
||||
fly.toml
|
||||
|
||||
# connectors
|
||||
/packages/core/connectors
|
||||
|
|
|
@ -19,7 +19,7 @@ tasks:
|
|||
pnpm start:dev
|
||||
env:
|
||||
TRUST_PROXY_HEADER: 1
|
||||
DB_URL: postgres://postgres:p0stgr3s@127.0.0.1:5432
|
||||
DB_URL: postgres://postgres:p0stgr3s@127.0.0.1:5432/logto
|
||||
|
||||
ports:
|
||||
- name: Logto
|
||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -32,6 +32,7 @@
|
|||
"silverhand",
|
||||
"slonik",
|
||||
"stylelint",
|
||||
"topbar"
|
||||
"topbar",
|
||||
"hasura"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"@logto/language-kit": "1.0.0-beta.20",
|
||||
"@logto/phrases": "workspace:^",
|
||||
"@logto/phrases-ui": "workspace:^",
|
||||
"@logto/react": "1.0.0-beta.10",
|
||||
"@logto/react": "1.0.0-beta.11",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@parcel/core": "2.7.0",
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useLogto } from '@logto/react';
|
|||
import type { RequestErrorBody } from '@logto/schemas';
|
||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||
import ky from 'ky';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -19,31 +19,27 @@ export class RequestError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const useToastError = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const toastError = async (response: Response) => {
|
||||
const fallbackErrorMessage = t('errors.unknown_server_error');
|
||||
|
||||
try {
|
||||
const data = await response.json<RequestErrorBody>();
|
||||
toast.error([data.message, data.details].join('\n') || fallbackErrorMessage);
|
||||
} catch {
|
||||
toast.error(fallbackErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
return toastError;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
hideErrorToast?: boolean;
|
||||
};
|
||||
|
||||
const useApi = ({ hideErrorToast }: Props = {}) => {
|
||||
const { isAuthenticated, getAccessToken } = useLogto();
|
||||
const { i18n } = useTranslation();
|
||||
const toastError = useToastError();
|
||||
const { t, i18n } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const toastError = useCallback(
|
||||
async (response: Response) => {
|
||||
const fallbackErrorMessage = t('errors.unknown_server_error');
|
||||
|
||||
try {
|
||||
const data = await response.json<RequestErrorBody>();
|
||||
toast.error([data.message, data.details].join('\n') || fallbackErrorMessage);
|
||||
} catch {
|
||||
toast.error(fallbackErrorMessage);
|
||||
}
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const api = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -196,12 +196,11 @@ export default function usePosition({
|
|||
|
||||
const anchorRect = anchorRef.current.getBoundingClientRect();
|
||||
const overlayRect = overlayRef.current.getBoundingClientRect();
|
||||
const { scrollTop, scrollLeft } = document.documentElement;
|
||||
|
||||
const verticalTop = anchorRect.y - overlayRect.height + scrollTop - offset.vertical;
|
||||
const verticalTop = anchorRect.y - overlayRect.height - offset.vertical;
|
||||
const verticalCenter =
|
||||
anchorRect.y - anchorRect.height / 2 - overlayRect.height / 2 + scrollTop + offset.vertical;
|
||||
const verticalBottom = anchorRect.y + anchorRect.height + scrollTop + offset.vertical;
|
||||
anchorRect.y - anchorRect.height / 2 - overlayRect.height / 2 + offset.vertical;
|
||||
const verticalBottom = anchorRect.y + anchorRect.height + offset.vertical;
|
||||
|
||||
const verticalPositionMap = {
|
||||
top: verticalTop,
|
||||
|
@ -209,11 +208,10 @@ export default function usePosition({
|
|||
bottom: verticalBottom,
|
||||
};
|
||||
|
||||
const horizontalStart = anchorRect.x + scrollLeft + offset.horizontal;
|
||||
const horizontalStart = anchorRect.x + offset.horizontal;
|
||||
const horizontalCenter =
|
||||
anchorRect.x + anchorRect.width / 2 - overlayRect.width / 2 + scrollLeft + offset.horizontal;
|
||||
const horizontalEnd =
|
||||
anchorRect.x + anchorRect.width - overlayRect.width + scrollLeft + offset.horizontal;
|
||||
anchorRect.x + anchorRect.width / 2 - overlayRect.width / 2 + offset.horizontal;
|
||||
const horizontalEnd = anchorRect.x + anchorRect.width - overlayRect.width + offset.horizontal;
|
||||
|
||||
const horizontalPositionMap = {
|
||||
start: horizontalStart,
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { IncomingHttpHeaders } from 'http';
|
|||
|
||||
import { UserRole } from '@logto/schemas';
|
||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { jwtVerify } from 'jose';
|
||||
import type { MiddlewareType, Request } from 'koa';
|
||||
|
@ -49,7 +50,7 @@ type TokenInfo = {
|
|||
// eslint-disable-next-line complexity
|
||||
export const verifyBearerTokenFromRequest = async (
|
||||
request: Request,
|
||||
resourceIndicator = managementResource.indicator
|
||||
resourceIndicator: Optional<string>
|
||||
): Promise<TokenInfo> => {
|
||||
const { isProduction, isIntegrationTest, developmentUserId } = envSet.values;
|
||||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||
|
@ -83,7 +84,10 @@ export default function koaAuth<StateT, ContextT extends IRouterParamContext, Re
|
|||
forRole?: UserRole
|
||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const { sub, clientId, roleNames } = await verifyBearerTokenFromRequest(ctx.request);
|
||||
const { sub, clientId, roleNames } = await verifyBearerTokenFromRequest(
|
||||
ctx.request,
|
||||
managementResource.indicator
|
||||
);
|
||||
|
||||
if (forRole) {
|
||||
assertThat(
|
||||
|
|
106
packages/core/src/routes/authn.test.ts
Normal file
106
packages/core/src/routes/authn.test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import RequestError from '@/errors/RequestError';
|
||||
import * as functions from '@/middleware/koa-auth';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import authnRoutes from './authn';
|
||||
|
||||
describe('authn route for Hasura', () => {
|
||||
const request = createRequester({ anonymousRoutes: authnRoutes });
|
||||
const mockUserId = 'foo';
|
||||
const mockExpectedRole = 'some_role';
|
||||
const mockUnauthorizedRole = 'V';
|
||||
const keys = Object.freeze({
|
||||
expectedRole: 'Expected-Role',
|
||||
hasuraUserId: 'X-Hasura-User-Id',
|
||||
hasuraRole: 'X-Hasura-Role',
|
||||
});
|
||||
|
||||
describe('with successful verification', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(functions, 'verifyBearerTokenFromRequest').mockResolvedValue({
|
||||
clientId: 'ok',
|
||||
sub: mockUserId,
|
||||
roleNames: [mockExpectedRole],
|
||||
});
|
||||
});
|
||||
|
||||
it('has expected role', async () => {
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io' })
|
||||
.set(keys.expectedRole, mockExpectedRole);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
[keys.hasuraUserId]: mockUserId,
|
||||
[keys.hasuraRole]: mockExpectedRole,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws 401 if no expected role present', async () => {
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io' })
|
||||
.set(keys.expectedRole, mockExpectedRole + '1');
|
||||
expect(response.status).toEqual(401);
|
||||
});
|
||||
|
||||
it('falls back to unauthorized role if no expected role present', async () => {
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io', unauthorizedRole: mockUnauthorizedRole })
|
||||
.set(keys.expectedRole, mockExpectedRole + '1');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
[keys.hasuraUserId]: mockUserId,
|
||||
[keys.hasuraRole]: mockUnauthorizedRole,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with failed verification', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(functions, 'verifyBearerTokenFromRequest')
|
||||
.mockImplementation(async (_, resource) => {
|
||||
if (resource) {
|
||||
throw new RequestError({ code: 'auth.jwt_sub_missing', status: 401 });
|
||||
}
|
||||
|
||||
return { clientId: 'not ok', sub: mockUserId };
|
||||
});
|
||||
});
|
||||
|
||||
it('throws 401 if no unauthorized role presents', async () => {
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io' })
|
||||
.set(keys.expectedRole, mockExpectedRole);
|
||||
expect(response.status).toEqual(401);
|
||||
});
|
||||
|
||||
it('falls back to unauthorized role with user id if no expected resource present', async () => {
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io', unauthorizedRole: mockUnauthorizedRole })
|
||||
.set(keys.expectedRole, mockExpectedRole);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
[keys.hasuraUserId]: mockUserId,
|
||||
[keys.hasuraRole]: mockUnauthorizedRole,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to unauthorized role if JWT is invalid', async () => {
|
||||
jest
|
||||
.spyOn(functions, 'verifyBearerTokenFromRequest')
|
||||
.mockRejectedValue(new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }));
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io', unauthorizedRole: mockUnauthorizedRole });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
[keys.hasuraRole]: mockUnauthorizedRole,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,15 +16,39 @@ export default function authnRoutes<T extends AnonymousRouter>(router: T) {
|
|||
router.get(
|
||||
'/authn/hasura',
|
||||
koaGuard({
|
||||
query: z.object({ resource: z.string().min(1) }),
|
||||
query: z.object({ resource: z.string().min(1), unauthorizedRole: z.string().optional() }),
|
||||
status: [200, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { resource, unauthorizedRole } = ctx.guard.query;
|
||||
const expectedRole = ctx.headers['expected-role']?.toString();
|
||||
const { sub, roleNames } = await verifyBearerTokenFromRequest(
|
||||
ctx.request,
|
||||
ctx.guard.query.resource
|
||||
);
|
||||
|
||||
const verifyToken = async (expectedResource?: string) => {
|
||||
try {
|
||||
return await verifyBearerTokenFromRequest(ctx.request, expectedResource);
|
||||
} catch {
|
||||
return {
|
||||
sub: undefined,
|
||||
roleNames: undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { sub, roleNames } = await verifyToken(resource);
|
||||
|
||||
if (unauthorizedRole && (!expectedRole || !roleNames?.includes(expectedRole))) {
|
||||
ctx.body = {
|
||||
'X-Hasura-User-Id':
|
||||
sub ??
|
||||
// When the previous token verification throws, the reason could be resource mismatch.
|
||||
// So we verify the token again with no resource provided.
|
||||
(await verifyToken().then(({ sub }) => sub)),
|
||||
'X-Hasura-Role': unauthorizedRole,
|
||||
};
|
||||
ctx.status = 200;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
if (expectedRole) {
|
||||
assertThat(
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"@logto/core-kit": "1.0.0-beta.20",
|
||||
"@logto/language-kit": "1.0.0-beta.20",
|
||||
"@logto/phrases": "workspace:^",
|
||||
"@logto/react": "1.0.0-beta.10",
|
||||
"@logto/react": "1.0.0-beta.11",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@parcel/core": "2.7.0",
|
||||
"@parcel/transformer-sass": "2.7.0",
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^29.1.2",
|
||||
"@logto/node": "1.0.0-beta.10",
|
||||
"@logto/node": "1.0.0-beta.11",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
|
|
|
@ -30,7 +30,7 @@ const translation = {
|
|||
got_it: '知道了',
|
||||
sign_in_with: '通过 {{name}} 登录',
|
||||
forgot_password: '重置密码',
|
||||
switch_to: '用{{method}}登录',
|
||||
switch_to: '切换到{{method}}',
|
||||
},
|
||||
description: {
|
||||
email: '邮箱',
|
||||
|
|
|
@ -67,7 +67,7 @@ const errors = {
|
|||
connector_id_mismatch: '传入的连接器 ID 与 session 中保存的记录不一致',
|
||||
connector_session_not_found: '无法找到连接器登录信息,请尝试重新登录。',
|
||||
verification_session_not_found: '验证失败,请重新验证。',
|
||||
verification_expired: '无密码验证已过期。请返回重新验证。',
|
||||
verification_expired: '当前页面已超时。为确保你的账号安全,请重新验证。',
|
||||
unauthorized: '请先登录',
|
||||
unsupported_prompt_name: '不支持的 prompt name',
|
||||
forgot_password_not_enabled: '忘记密码功能没有开启。',
|
||||
|
|
|
@ -55,21 +55,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.outline {
|
||||
border: _.border(var(--color-brand-default));
|
||||
background: transparent;
|
||||
color: var(--color-type-link);
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
border-color: var(--color-type-disable);
|
||||
color: var(--color-type-disable);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--color-overlay-brand-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.desktop) {
|
||||
.primary {
|
||||
|
@ -91,14 +76,4 @@
|
|||
background: var(--color-overlay-neutral-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.outline {
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--color-overlay-brand-focused);
|
||||
}
|
||||
|
||||
&:not(:disabled):not(:active):hover {
|
||||
background: var(--color-overlay-brand-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type ButtonType = 'primary' | 'secondary' | 'outline';
|
||||
export type ButtonType = 'primary' | 'secondary';
|
||||
|
||||
type BaseProps = Omit<HTMLProps<HTMLButtonElement>, 'type' | 'size' | 'title'> & {
|
||||
htmlType?: 'button' | 'submit' | 'reset';
|
||||
|
|
|
@ -37,7 +37,7 @@ const AcModal = ({
|
|||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.footer}>
|
||||
<Button title={cancelText} type="outline" size="small" onClick={onClose} />
|
||||
<Button title={cancelText} type="secondary" size="small" onClick={onClose} />
|
||||
{onConfirm && <Button title={confirmText} size="small" onClick={onConfirm} />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,13 +35,6 @@
|
|||
&::placeholder {
|
||||
color: var(--color-type-secondary);
|
||||
}
|
||||
|
||||
// Overwrite webkit auto-fill style
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0 30px var(--color-bg-body) inset;
|
||||
-webkit-text-fill-color: var(--color-type-primary);
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
|
|
|
@ -4,20 +4,30 @@
|
|||
padding: _.unit(3) _.unit(4);
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-type-primary);
|
||||
background: var(--color-alert-99);
|
||||
margin: 0 auto _.unit(2);
|
||||
@include _.flex_row;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-alert-70);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: _.unit(3);
|
||||
&.alert {
|
||||
background: var(--color-alert-99);
|
||||
|
||||
.icon {
|
||||
color: var(--color-alert-70);
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: var(--color-neutral-variant-80);
|
||||
|
||||
.icon {
|
||||
color: var(--color-neutral-variant-60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
|
@ -26,8 +36,6 @@
|
|||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
|
@ -35,10 +43,4 @@
|
|||
.notification {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.link {
|
||||
&:hover {
|
||||
color: var(--color-brand-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,23 @@
|
|||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import InfoIcon from '@/assets/icons/info-icon.svg';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import TextLink from '../TextLink';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
type?: 'info' | 'alert';
|
||||
};
|
||||
|
||||
const Notification = ({ className, message, onClose }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Notification = ({ className, message, onClose, type = 'info' }: Props) => {
|
||||
return (
|
||||
<div className={classNames(styles.notification, className)}>
|
||||
<div className={classNames(styles.notification, styles[type], className)}>
|
||||
<InfoIcon className={styles.icon} />
|
||||
<div className={styles.message}>{message}</div>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.link}
|
||||
onClick={onClose}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Esc: onClose,
|
||||
Enter: onClose,
|
||||
' ': onClose,
|
||||
})}
|
||||
>
|
||||
{t('action.got_it')}
|
||||
</a>
|
||||
<TextLink text="action.got_it" className={styles.link} onClick={onClose} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
color: var(--color-brand-default);
|
||||
text-decoration: none;
|
||||
font: var(--font-label-2);
|
||||
border-radius: _.unit(1);
|
||||
padding: _.unit(1) _.unit(0.5);
|
||||
|
||||
&:active {
|
||||
background: var(--color-overlay-brand-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
|
@ -21,10 +27,15 @@
|
|||
:global(body.desktop) {
|
||||
.link {
|
||||
&.primary:hover {
|
||||
text-decoration: underline;
|
||||
background: var(--color-overlay-brand-hover);
|
||||
}
|
||||
|
||||
&.secondary:hover {
|
||||
&.primary:focus-visible {
|
||||
outline: _.border(var(--color-overlay-brand-focused));
|
||||
}
|
||||
|
||||
&.secondary:hover,
|
||||
&.secondary:active {
|
||||
color: var(--color-brand-default);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
--color-neutral-95: #eff1f1;
|
||||
--color-neutral-100: #fff;
|
||||
|
||||
--color-neutral-variant-60: #928f9a;
|
||||
--color-neutral-variant-80: #e5e1ec;
|
||||
|
||||
--color-danger-30: #930006;
|
||||
|
@ -104,6 +105,7 @@
|
|||
--color-neutral-99: #191c1d;
|
||||
--color-neutral-100: #000;
|
||||
|
||||
--color-neutral-variant-60: #928f9a;
|
||||
--color-neutral-variant-80: #5f5d67;
|
||||
--color-neutral-variant-90: #47464e;
|
||||
|
||||
|
|
|
@ -18,6 +18,9 @@ export const onKeyDownHandler =
|
|||
|
||||
if (typeof callback === 'object') {
|
||||
callback[key]?.(event);
|
||||
event.preventDefault();
|
||||
|
||||
if (callback[key]) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -109,7 +109,7 @@ importers:
|
|||
'@logto/language-kit': 1.0.0-beta.20
|
||||
'@logto/phrases': workspace:^
|
||||
'@logto/phrases-ui': workspace:^
|
||||
'@logto/react': 1.0.0-beta.10
|
||||
'@logto/react': 1.0.0-beta.11
|
||||
'@logto/schemas': workspace:^
|
||||
'@mdx-js/react': ^1.6.22
|
||||
'@parcel/core': 2.7.0
|
||||
|
@ -180,7 +180,7 @@ importers:
|
|||
'@logto/language-kit': 1.0.0-beta.20
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/phrases-ui': link:../phrases-ui
|
||||
'@logto/react': 1.0.0-beta.10_react@18.2.0
|
||||
'@logto/react': 1.0.0-beta.11_react@18.2.0
|
||||
'@logto/schemas': link:../schemas
|
||||
'@mdx-js/react': 1.6.22_react@18.2.0
|
||||
'@parcel/core': 2.7.0
|
||||
|
@ -416,7 +416,7 @@ importers:
|
|||
'@logto/core-kit': 1.0.0-beta.20
|
||||
'@logto/language-kit': 1.0.0-beta.20
|
||||
'@logto/phrases': workspace:^
|
||||
'@logto/react': 1.0.0-beta.10
|
||||
'@logto/react': 1.0.0-beta.11
|
||||
'@logto/schemas': workspace:^
|
||||
'@parcel/core': 2.7.0
|
||||
'@parcel/transformer-sass': 2.7.0
|
||||
|
@ -443,7 +443,7 @@ importers:
|
|||
'@logto/core-kit': 1.0.0-beta.20
|
||||
'@logto/language-kit': 1.0.0-beta.20
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/react': 1.0.0-beta.10_react@18.2.0
|
||||
'@logto/react': 1.0.0-beta.11_react@18.2.0
|
||||
'@logto/schemas': link:../schemas
|
||||
'@parcel/core': 2.7.0
|
||||
'@parcel/transformer-sass': 2.7.0_@parcel+core@2.7.0
|
||||
|
@ -470,7 +470,7 @@ importers:
|
|||
packages/integration-tests:
|
||||
specifiers:
|
||||
'@jest/types': ^29.1.2
|
||||
'@logto/node': 1.0.0-beta.10
|
||||
'@logto/node': 1.0.0-beta.11
|
||||
'@logto/schemas': workspace:^
|
||||
'@peculiar/webcrypto': ^1.3.3
|
||||
'@silverhand/eslint-config': 1.3.0
|
||||
|
@ -495,7 +495,7 @@ importers:
|
|||
typescript: ^4.7.4
|
||||
devDependencies:
|
||||
'@jest/types': 29.1.2
|
||||
'@logto/node': 1.0.0-beta.10
|
||||
'@logto/node': 1.0.0-beta.11
|
||||
'@logto/schemas': link:../schemas
|
||||
'@peculiar/webcrypto': 1.3.3
|
||||
'@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni
|
||||
|
@ -2300,19 +2300,19 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@logto/browser/1.0.0-beta.10:
|
||||
resolution: {integrity: sha512-ziZv8TTWwzK9PgBtioF9Wplfaj0J/InyxSBmfgFS5PX54GAVfOy3uGBi9hGL6HDRPj3SYs2U1bi21YPlF9z/8w==}
|
||||
/@logto/browser/1.0.0-beta.11:
|
||||
resolution: {integrity: sha512-Ofdj5UqLzwoW66XGnff+1U6Lix90qI2Q30+TISJz6soCm97oqJdWpY2SRlMAfnvDobsFPqAxyCAz49Cg16h50Q==}
|
||||
dependencies:
|
||||
'@logto/client': 1.0.0-beta.10
|
||||
'@logto/client': 1.0.0-beta.11
|
||||
'@silverhand/essentials': 1.3.0
|
||||
js-base64: 3.7.2
|
||||
dev: true
|
||||
|
||||
/@logto/client/1.0.0-beta.10:
|
||||
resolution: {integrity: sha512-XHkOJdvxsBix/8cZ3a6Hx3UiiQkaJBDF9D7BGM8lZxPz5nsGq8YkX5ZRvjVCX9Y3fE3nRb5Iv1ccWvV192lUWg==}
|
||||
/@logto/client/1.0.0-beta.11:
|
||||
resolution: {integrity: sha512-7Nl+53JPgB0wjMU9zJH6SrOj4OKn0Kl9U1cumt/IHSYuaDyV7TgvR/HaaeSZuZ0JxbMg66Riee87Qx9Tv46k6A==}
|
||||
dependencies:
|
||||
'@logto/core-kit': 1.0.0-beta.19
|
||||
'@logto/js': 1.0.0-beta.10
|
||||
'@logto/core-kit': 1.0.0-beta.20
|
||||
'@logto/js': 1.0.0-beta.11
|
||||
'@silverhand/essentials': 1.3.0
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.6.0
|
||||
|
@ -2340,16 +2340,6 @@ packages:
|
|||
zod: 3.19.1
|
||||
dev: false
|
||||
|
||||
/@logto/core-kit/1.0.0-beta.19:
|
||||
resolution: {integrity: sha512-cqwfz+Ic/t7mV23QUEXWeRaLTqN71NSv02adaul8VWZQg4IePOlWLLaiTyY6ods88f3ZOarIBlJ5fYexw3J8qg==}
|
||||
engines: {node: ^16.0.0}
|
||||
dependencies:
|
||||
'@logto/language-kit': 1.0.0-beta.20
|
||||
color: 4.2.3
|
||||
nanoid: 3.3.4
|
||||
zod: 3.19.1
|
||||
dev: true
|
||||
|
||||
/@logto/core-kit/1.0.0-beta.20:
|
||||
resolution: {integrity: sha512-seYvL/aGYRfO4d0FYfKIW/Cu9PnFMRpRM5/oRXwXbcbv+LY1a3TcAX0itrVXeBygIrxiAmWd9DL7CGIWzb48Qg==}
|
||||
engines: {node: ^16.0.0}
|
||||
|
@ -2359,10 +2349,10 @@ packages:
|
|||
nanoid: 3.3.4
|
||||
zod: 3.19.1
|
||||
|
||||
/@logto/js/1.0.0-beta.10:
|
||||
resolution: {integrity: sha512-mMzverjbeKtGjSb0NmEUHzDBRrXhCPOydCE37yhzL/qORiehyblPqntw3lLrf5oCNUKaxv7PzT5q/Lfbxb3Q8g==}
|
||||
/@logto/js/1.0.0-beta.11:
|
||||
resolution: {integrity: sha512-V0cV+T+DFcpqAAIjfdiEuEYG+ePASR7VbPM1zMLv02S/5zz3pStedbLrksZaSVUOmQvsieEF5d5/kZ8ZshQa0A==}
|
||||
dependencies:
|
||||
'@logto/core-kit': 1.0.0-beta.19
|
||||
'@logto/core-kit': 1.0.0-beta.20
|
||||
'@silverhand/essentials': 1.3.0
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.6.0
|
||||
|
@ -2375,10 +2365,10 @@ packages:
|
|||
dependencies:
|
||||
zod: 3.19.1
|
||||
|
||||
/@logto/node/1.0.0-beta.10:
|
||||
resolution: {integrity: sha512-4st77cD1h/bCIrt+BttDHva/I7ibLw0ilvi30uadEwuH0Ih8WFYvlyi2V1wOljd5Ym+fqSMbyKXQQIwba6CenQ==}
|
||||
/@logto/node/1.0.0-beta.11:
|
||||
resolution: {integrity: sha512-nsa9RtrzBRmLMJNeNzutJ72bLwZpGUr4lhBAze7YXJi6o9ujvZOLgdvZAZeoSdxxBhTilwbDWNXmj8n3Ndavaw==}
|
||||
dependencies:
|
||||
'@logto/client': 1.0.0-beta.10
|
||||
'@logto/client': 1.0.0-beta.11
|
||||
'@silverhand/essentials': 1.3.0
|
||||
js-base64: 3.7.2
|
||||
node-fetch: 2.6.7
|
||||
|
@ -2386,12 +2376,12 @@ packages:
|
|||
- encoding
|
||||
dev: true
|
||||
|
||||
/@logto/react/1.0.0-beta.10_react@18.2.0:
|
||||
resolution: {integrity: sha512-MutQplD5VkUqYIhnaQgNiRpbQTRK/OS2Ut70j99mVax/frImlIu1m1YeuE8NEqGmB9SBQvJoP5kECxVNf2a4Nw==}
|
||||
/@logto/react/1.0.0-beta.11_react@18.2.0:
|
||||
resolution: {integrity: sha512-W9L1QrJml4tHlrUkmdaJRsj4ln+SgwGAeyF+QhmKBq5CoSR+EauE63ySLhPn2KFVrc3m/sioejJIVy+1sUe21w==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0 || ^18.0.0'
|
||||
dependencies:
|
||||
'@logto/browser': 1.0.0-beta.10
|
||||
'@logto/browser': 1.0.0-beta.11
|
||||
'@silverhand/essentials': 1.3.0
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue