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:
|
main-lint:
|
||||||
# avoid out of memory issue since macOS has bigger memory
|
# 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
|
# 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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -29,6 +29,7 @@ cache
|
||||||
.idea/
|
.idea/
|
||||||
*.pem
|
*.pem
|
||||||
.history
|
.history
|
||||||
|
fly.toml
|
||||||
|
|
||||||
# connectors
|
# connectors
|
||||||
/packages/core/connectors
|
/packages/core/connectors
|
||||||
|
|
|
@ -19,7 +19,7 @@ tasks:
|
||||||
pnpm start:dev
|
pnpm start:dev
|
||||||
env:
|
env:
|
||||||
TRUST_PROXY_HEADER: 1
|
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:
|
ports:
|
||||||
- name: Logto
|
- name: Logto
|
||||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -32,6 +32,7 @@
|
||||||
"silverhand",
|
"silverhand",
|
||||||
"slonik",
|
"slonik",
|
||||||
"stylelint",
|
"stylelint",
|
||||||
"topbar"
|
"topbar",
|
||||||
|
"hasura"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"@logto/language-kit": "1.0.0-beta.20",
|
"@logto/language-kit": "1.0.0-beta.20",
|
||||||
"@logto/phrases": "workspace:^",
|
"@logto/phrases": "workspace:^",
|
||||||
"@logto/phrases-ui": "workspace:^",
|
"@logto/phrases-ui": "workspace:^",
|
||||||
"@logto/react": "1.0.0-beta.10",
|
"@logto/react": "1.0.0-beta.11",
|
||||||
"@logto/schemas": "workspace:^",
|
"@logto/schemas": "workspace:^",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"@parcel/core": "2.7.0",
|
"@parcel/core": "2.7.0",
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useLogto } from '@logto/react';
|
||||||
import type { RequestErrorBody } from '@logto/schemas';
|
import type { RequestErrorBody } from '@logto/schemas';
|
||||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||||
import ky from 'ky';
|
import ky from 'ky';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -19,10 +19,16 @@ export class RequestError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useToastError = () => {
|
type Props = {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
hideErrorToast?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const toastError = async (response: Response) => {
|
const useApi = ({ hideErrorToast }: Props = {}) => {
|
||||||
|
const { isAuthenticated, getAccessToken } = useLogto();
|
||||||
|
const { t, i18n } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const toastError = useCallback(
|
||||||
|
async (response: Response) => {
|
||||||
const fallbackErrorMessage = t('errors.unknown_server_error');
|
const fallbackErrorMessage = t('errors.unknown_server_error');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -31,19 +37,9 @@ const useToastError = () => {
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(fallbackErrorMessage);
|
toast.error(fallbackErrorMessage);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[t]
|
||||||
return toastError;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
hideErrorToast?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useApi = ({ hideErrorToast }: Props = {}) => {
|
|
||||||
const { isAuthenticated, getAccessToken } = useLogto();
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const toastError = useToastError();
|
|
||||||
|
|
||||||
const api = useMemo(
|
const api = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
@ -196,12 +196,11 @@ export default function usePosition({
|
||||||
|
|
||||||
const anchorRect = anchorRef.current.getBoundingClientRect();
|
const anchorRect = anchorRef.current.getBoundingClientRect();
|
||||||
const overlayRect = overlayRef.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 =
|
const verticalCenter =
|
||||||
anchorRect.y - anchorRect.height / 2 - overlayRect.height / 2 + scrollTop + offset.vertical;
|
anchorRect.y - anchorRect.height / 2 - overlayRect.height / 2 + offset.vertical;
|
||||||
const verticalBottom = anchorRect.y + anchorRect.height + scrollTop + offset.vertical;
|
const verticalBottom = anchorRect.y + anchorRect.height + offset.vertical;
|
||||||
|
|
||||||
const verticalPositionMap = {
|
const verticalPositionMap = {
|
||||||
top: verticalTop,
|
top: verticalTop,
|
||||||
|
@ -209,11 +208,10 @@ export default function usePosition({
|
||||||
bottom: verticalBottom,
|
bottom: verticalBottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
const horizontalStart = anchorRect.x + scrollLeft + offset.horizontal;
|
const horizontalStart = anchorRect.x + offset.horizontal;
|
||||||
const horizontalCenter =
|
const horizontalCenter =
|
||||||
anchorRect.x + anchorRect.width / 2 - overlayRect.width / 2 + scrollLeft + offset.horizontal;
|
anchorRect.x + anchorRect.width / 2 - overlayRect.width / 2 + offset.horizontal;
|
||||||
const horizontalEnd =
|
const horizontalEnd = anchorRect.x + anchorRect.width - overlayRect.width + offset.horizontal;
|
||||||
anchorRect.x + anchorRect.width - overlayRect.width + scrollLeft + offset.horizontal;
|
|
||||||
|
|
||||||
const horizontalPositionMap = {
|
const horizontalPositionMap = {
|
||||||
start: horizontalStart,
|
start: horizontalStart,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { IncomingHttpHeaders } from 'http';
|
||||||
|
|
||||||
import { UserRole } from '@logto/schemas';
|
import { UserRole } from '@logto/schemas';
|
||||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||||
|
import type { Optional } from '@silverhand/essentials';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { jwtVerify } from 'jose';
|
import { jwtVerify } from 'jose';
|
||||||
import type { MiddlewareType, Request } from 'koa';
|
import type { MiddlewareType, Request } from 'koa';
|
||||||
|
@ -49,7 +50,7 @@ type TokenInfo = {
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
export const verifyBearerTokenFromRequest = async (
|
export const verifyBearerTokenFromRequest = async (
|
||||||
request: Request,
|
request: Request,
|
||||||
resourceIndicator = managementResource.indicator
|
resourceIndicator: Optional<string>
|
||||||
): Promise<TokenInfo> => {
|
): Promise<TokenInfo> => {
|
||||||
const { isProduction, isIntegrationTest, developmentUserId } = envSet.values;
|
const { isProduction, isIntegrationTest, developmentUserId } = envSet.values;
|
||||||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||||
|
@ -83,7 +84,10 @@ export default function koaAuth<StateT, ContextT extends IRouterParamContext, Re
|
||||||
forRole?: UserRole
|
forRole?: UserRole
|
||||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const { sub, clientId, roleNames } = await verifyBearerTokenFromRequest(ctx.request);
|
const { sub, clientId, roleNames } = await verifyBearerTokenFromRequest(
|
||||||
|
ctx.request,
|
||||||
|
managementResource.indicator
|
||||||
|
);
|
||||||
|
|
||||||
if (forRole) {
|
if (forRole) {
|
||||||
assertThat(
|
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(
|
router.get(
|
||||||
'/authn/hasura',
|
'/authn/hasura',
|
||||||
koaGuard({
|
koaGuard({
|
||||||
query: z.object({ resource: z.string().min(1) }),
|
query: z.object({ resource: z.string().min(1), unauthorizedRole: z.string().optional() }),
|
||||||
status: [200, 401],
|
status: [200, 401],
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
|
const { resource, unauthorizedRole } = ctx.guard.query;
|
||||||
const expectedRole = ctx.headers['expected-role']?.toString();
|
const expectedRole = ctx.headers['expected-role']?.toString();
|
||||||
const { sub, roleNames } = await verifyBearerTokenFromRequest(
|
|
||||||
ctx.request,
|
const verifyToken = async (expectedResource?: string) => {
|
||||||
ctx.guard.query.resource
|
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) {
|
if (expectedRole) {
|
||||||
assertThat(
|
assertThat(
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"@logto/core-kit": "1.0.0-beta.20",
|
"@logto/core-kit": "1.0.0-beta.20",
|
||||||
"@logto/language-kit": "1.0.0-beta.20",
|
"@logto/language-kit": "1.0.0-beta.20",
|
||||||
"@logto/phrases": "workspace:^",
|
"@logto/phrases": "workspace:^",
|
||||||
"@logto/react": "1.0.0-beta.10",
|
"@logto/react": "1.0.0-beta.11",
|
||||||
"@logto/schemas": "workspace:^",
|
"@logto/schemas": "workspace:^",
|
||||||
"@parcel/core": "2.7.0",
|
"@parcel/core": "2.7.0",
|
||||||
"@parcel/transformer-sass": "2.7.0",
|
"@parcel/transformer-sass": "2.7.0",
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/types": "^29.1.2",
|
"@jest/types": "^29.1.2",
|
||||||
"@logto/node": "1.0.0-beta.10",
|
"@logto/node": "1.0.0-beta.11",
|
||||||
"@logto/schemas": "workspace:^",
|
"@logto/schemas": "workspace:^",
|
||||||
"@peculiar/webcrypto": "^1.3.3",
|
"@peculiar/webcrypto": "^1.3.3",
|
||||||
"@silverhand/eslint-config": "1.3.0",
|
"@silverhand/eslint-config": "1.3.0",
|
||||||
|
|
|
@ -30,7 +30,7 @@ const translation = {
|
||||||
got_it: '知道了',
|
got_it: '知道了',
|
||||||
sign_in_with: '通过 {{name}} 登录',
|
sign_in_with: '通过 {{name}} 登录',
|
||||||
forgot_password: '重置密码',
|
forgot_password: '重置密码',
|
||||||
switch_to: '用{{method}}登录',
|
switch_to: '切换到{{method}}',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
|
|
|
@ -67,7 +67,7 @@ const errors = {
|
||||||
connector_id_mismatch: '传入的连接器 ID 与 session 中保存的记录不一致',
|
connector_id_mismatch: '传入的连接器 ID 与 session 中保存的记录不一致',
|
||||||
connector_session_not_found: '无法找到连接器登录信息,请尝试重新登录。',
|
connector_session_not_found: '无法找到连接器登录信息,请尝试重新登录。',
|
||||||
verification_session_not_found: '验证失败,请重新验证。',
|
verification_session_not_found: '验证失败,请重新验证。',
|
||||||
verification_expired: '无密码验证已过期。请返回重新验证。',
|
verification_expired: '当前页面已超时。为确保你的账号安全,请重新验证。',
|
||||||
unauthorized: '请先登录',
|
unauthorized: '请先登录',
|
||||||
unsupported_prompt_name: '不支持的 prompt name',
|
unsupported_prompt_name: '不支持的 prompt name',
|
||||||
forgot_password_not_enabled: '忘记密码功能没有开启。',
|
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) {
|
:global(body.desktop) {
|
||||||
.primary {
|
.primary {
|
||||||
|
@ -91,14 +76,4 @@
|
||||||
background: var(--color-overlay-neutral-hover);
|
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';
|
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'> & {
|
type BaseProps = Omit<HTMLProps<HTMLButtonElement>, 'type' | 'size' | 'title'> & {
|
||||||
htmlType?: 'button' | 'submit' | 'reset';
|
htmlType?: 'button' | 'submit' | 'reset';
|
||||||
|
|
|
@ -37,7 +37,7 @@ const AcModal = ({
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>{children}</div>
|
||||||
<div className={styles.footer}>
|
<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} />}
|
{onConfirm && <Button title={confirmText} size="small" onClick={onConfirm} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,13 +35,6 @@
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--color-type-secondary);
|
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 {
|
&:focus-within {
|
||||||
|
|
|
@ -4,20 +4,30 @@
|
||||||
padding: _.unit(3) _.unit(4);
|
padding: _.unit(3) _.unit(4);
|
||||||
font: var(--font-body-2);
|
font: var(--font-body-2);
|
||||||
color: var(--color-type-primary);
|
color: var(--color-type-primary);
|
||||||
background: var(--color-alert-99);
|
|
||||||
margin: 0 auto _.unit(2);
|
margin: 0 auto _.unit(2);
|
||||||
@include _.flex_row;
|
@include _.flex_row;
|
||||||
|
|
||||||
&:focus-visible {
|
.icon {
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--color-alert-70);
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-right: _.unit(3);
|
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 {
|
.message {
|
||||||
|
@ -26,8 +36,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,10 +43,4 @@
|
||||||
.notification {
|
.notification {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-brand-default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,23 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import InfoIcon from '@/assets/icons/info-icon.svg';
|
import InfoIcon from '@/assets/icons/info-icon.svg';
|
||||||
import { onKeyDownHandler } from '@/utils/a11y';
|
|
||||||
|
|
||||||
|
import TextLink from '../TextLink';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
message: string;
|
message: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
type?: 'info' | 'alert';
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notification = ({ className, message, onClose }: Props) => {
|
const Notification = ({ className, message, onClose, type = 'info' }: Props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.notification, className)}>
|
<div className={classNames(styles.notification, styles[type], className)}>
|
||||||
<InfoIcon className={styles.icon} />
|
<InfoIcon className={styles.icon} />
|
||||||
<div className={styles.message}>{message}</div>
|
<div className={styles.message}>{message}</div>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
<TextLink text="action.got_it" className={styles.link} onClick={onClose} />
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className={styles.link}
|
|
||||||
onClick={onClose}
|
|
||||||
onKeyDown={onKeyDownHandler({
|
|
||||||
Esc: onClose,
|
|
||||||
Enter: onClose,
|
|
||||||
' ': onClose,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('action.got_it')}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,12 @@
|
||||||
color: var(--color-brand-default);
|
color: var(--color-brand-default);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font: var(--font-label-2);
|
font: var(--font-label-2);
|
||||||
|
border-radius: _.unit(1);
|
||||||
|
padding: _.unit(1) _.unit(0.5);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--color-overlay-brand-pressed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.secondary {
|
&.secondary {
|
||||||
|
@ -21,10 +27,15 @@
|
||||||
:global(body.desktop) {
|
:global(body.desktop) {
|
||||||
.link {
|
.link {
|
||||||
&.primary:hover {
|
&.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);
|
color: var(--color-brand-default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
--color-neutral-95: #eff1f1;
|
--color-neutral-95: #eff1f1;
|
||||||
--color-neutral-100: #fff;
|
--color-neutral-100: #fff;
|
||||||
|
|
||||||
|
--color-neutral-variant-60: #928f9a;
|
||||||
--color-neutral-variant-80: #e5e1ec;
|
--color-neutral-variant-80: #e5e1ec;
|
||||||
|
|
||||||
--color-danger-30: #930006;
|
--color-danger-30: #930006;
|
||||||
|
@ -104,6 +105,7 @@
|
||||||
--color-neutral-99: #191c1d;
|
--color-neutral-99: #191c1d;
|
||||||
--color-neutral-100: #000;
|
--color-neutral-100: #000;
|
||||||
|
|
||||||
|
--color-neutral-variant-60: #928f9a;
|
||||||
--color-neutral-variant-80: #5f5d67;
|
--color-neutral-variant-80: #5f5d67;
|
||||||
--color-neutral-variant-90: #47464e;
|
--color-neutral-variant-90: #47464e;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,9 @@ export const onKeyDownHandler =
|
||||||
|
|
||||||
if (typeof callback === 'object') {
|
if (typeof callback === 'object') {
|
||||||
callback[key]?.(event);
|
callback[key]?.(event);
|
||||||
|
|
||||||
|
if (callback[key]) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -109,7 +109,7 @@ importers:
|
||||||
'@logto/language-kit': 1.0.0-beta.20
|
'@logto/language-kit': 1.0.0-beta.20
|
||||||
'@logto/phrases': workspace:^
|
'@logto/phrases': workspace:^
|
||||||
'@logto/phrases-ui': workspace:^
|
'@logto/phrases-ui': workspace:^
|
||||||
'@logto/react': 1.0.0-beta.10
|
'@logto/react': 1.0.0-beta.11
|
||||||
'@logto/schemas': workspace:^
|
'@logto/schemas': workspace:^
|
||||||
'@mdx-js/react': ^1.6.22
|
'@mdx-js/react': ^1.6.22
|
||||||
'@parcel/core': 2.7.0
|
'@parcel/core': 2.7.0
|
||||||
|
@ -180,7 +180,7 @@ importers:
|
||||||
'@logto/language-kit': 1.0.0-beta.20
|
'@logto/language-kit': 1.0.0-beta.20
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
'@logto/phrases-ui': link:../phrases-ui
|
'@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
|
'@logto/schemas': link:../schemas
|
||||||
'@mdx-js/react': 1.6.22_react@18.2.0
|
'@mdx-js/react': 1.6.22_react@18.2.0
|
||||||
'@parcel/core': 2.7.0
|
'@parcel/core': 2.7.0
|
||||||
|
@ -416,7 +416,7 @@ importers:
|
||||||
'@logto/core-kit': 1.0.0-beta.20
|
'@logto/core-kit': 1.0.0-beta.20
|
||||||
'@logto/language-kit': 1.0.0-beta.20
|
'@logto/language-kit': 1.0.0-beta.20
|
||||||
'@logto/phrases': workspace:^
|
'@logto/phrases': workspace:^
|
||||||
'@logto/react': 1.0.0-beta.10
|
'@logto/react': 1.0.0-beta.11
|
||||||
'@logto/schemas': workspace:^
|
'@logto/schemas': workspace:^
|
||||||
'@parcel/core': 2.7.0
|
'@parcel/core': 2.7.0
|
||||||
'@parcel/transformer-sass': 2.7.0
|
'@parcel/transformer-sass': 2.7.0
|
||||||
|
@ -443,7 +443,7 @@ importers:
|
||||||
'@logto/core-kit': 1.0.0-beta.20
|
'@logto/core-kit': 1.0.0-beta.20
|
||||||
'@logto/language-kit': 1.0.0-beta.20
|
'@logto/language-kit': 1.0.0-beta.20
|
||||||
'@logto/phrases': link:../phrases
|
'@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
|
'@logto/schemas': link:../schemas
|
||||||
'@parcel/core': 2.7.0
|
'@parcel/core': 2.7.0
|
||||||
'@parcel/transformer-sass': 2.7.0_@parcel+core@2.7.0
|
'@parcel/transformer-sass': 2.7.0_@parcel+core@2.7.0
|
||||||
|
@ -470,7 +470,7 @@ importers:
|
||||||
packages/integration-tests:
|
packages/integration-tests:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@jest/types': ^29.1.2
|
'@jest/types': ^29.1.2
|
||||||
'@logto/node': 1.0.0-beta.10
|
'@logto/node': 1.0.0-beta.11
|
||||||
'@logto/schemas': workspace:^
|
'@logto/schemas': workspace:^
|
||||||
'@peculiar/webcrypto': ^1.3.3
|
'@peculiar/webcrypto': ^1.3.3
|
||||||
'@silverhand/eslint-config': 1.3.0
|
'@silverhand/eslint-config': 1.3.0
|
||||||
|
@ -495,7 +495,7 @@ importers:
|
||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@jest/types': 29.1.2
|
'@jest/types': 29.1.2
|
||||||
'@logto/node': 1.0.0-beta.10
|
'@logto/node': 1.0.0-beta.11
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
'@peculiar/webcrypto': 1.3.3
|
'@peculiar/webcrypto': 1.3.3
|
||||||
'@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni
|
'@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni
|
||||||
|
@ -2300,19 +2300,19 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@logto/browser/1.0.0-beta.10:
|
/@logto/browser/1.0.0-beta.11:
|
||||||
resolution: {integrity: sha512-ziZv8TTWwzK9PgBtioF9Wplfaj0J/InyxSBmfgFS5PX54GAVfOy3uGBi9hGL6HDRPj3SYs2U1bi21YPlF9z/8w==}
|
resolution: {integrity: sha512-Ofdj5UqLzwoW66XGnff+1U6Lix90qI2Q30+TISJz6soCm97oqJdWpY2SRlMAfnvDobsFPqAxyCAz49Cg16h50Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/client': 1.0.0-beta.10
|
'@logto/client': 1.0.0-beta.11
|
||||||
'@silverhand/essentials': 1.3.0
|
'@silverhand/essentials': 1.3.0
|
||||||
js-base64: 3.7.2
|
js-base64: 3.7.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/client/1.0.0-beta.10:
|
/@logto/client/1.0.0-beta.11:
|
||||||
resolution: {integrity: sha512-XHkOJdvxsBix/8cZ3a6Hx3UiiQkaJBDF9D7BGM8lZxPz5nsGq8YkX5ZRvjVCX9Y3fE3nRb5Iv1ccWvV192lUWg==}
|
resolution: {integrity: sha512-7Nl+53JPgB0wjMU9zJH6SrOj4OKn0Kl9U1cumt/IHSYuaDyV7TgvR/HaaeSZuZ0JxbMg66Riee87Qx9Tv46k6A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/core-kit': 1.0.0-beta.19
|
'@logto/core-kit': 1.0.0-beta.20
|
||||||
'@logto/js': 1.0.0-beta.10
|
'@logto/js': 1.0.0-beta.11
|
||||||
'@silverhand/essentials': 1.3.0
|
'@silverhand/essentials': 1.3.0
|
||||||
camelcase-keys: 7.0.2
|
camelcase-keys: 7.0.2
|
||||||
jose: 4.6.0
|
jose: 4.6.0
|
||||||
|
@ -2340,16 +2340,6 @@ packages:
|
||||||
zod: 3.19.1
|
zod: 3.19.1
|
||||||
dev: false
|
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:
|
/@logto/core-kit/1.0.0-beta.20:
|
||||||
resolution: {integrity: sha512-seYvL/aGYRfO4d0FYfKIW/Cu9PnFMRpRM5/oRXwXbcbv+LY1a3TcAX0itrVXeBygIrxiAmWd9DL7CGIWzb48Qg==}
|
resolution: {integrity: sha512-seYvL/aGYRfO4d0FYfKIW/Cu9PnFMRpRM5/oRXwXbcbv+LY1a3TcAX0itrVXeBygIrxiAmWd9DL7CGIWzb48Qg==}
|
||||||
engines: {node: ^16.0.0}
|
engines: {node: ^16.0.0}
|
||||||
|
@ -2359,10 +2349,10 @@ packages:
|
||||||
nanoid: 3.3.4
|
nanoid: 3.3.4
|
||||||
zod: 3.19.1
|
zod: 3.19.1
|
||||||
|
|
||||||
/@logto/js/1.0.0-beta.10:
|
/@logto/js/1.0.0-beta.11:
|
||||||
resolution: {integrity: sha512-mMzverjbeKtGjSb0NmEUHzDBRrXhCPOydCE37yhzL/qORiehyblPqntw3lLrf5oCNUKaxv7PzT5q/Lfbxb3Q8g==}
|
resolution: {integrity: sha512-V0cV+T+DFcpqAAIjfdiEuEYG+ePASR7VbPM1zMLv02S/5zz3pStedbLrksZaSVUOmQvsieEF5d5/kZ8ZshQa0A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/core-kit': 1.0.0-beta.19
|
'@logto/core-kit': 1.0.0-beta.20
|
||||||
'@silverhand/essentials': 1.3.0
|
'@silverhand/essentials': 1.3.0
|
||||||
camelcase-keys: 7.0.2
|
camelcase-keys: 7.0.2
|
||||||
jose: 4.6.0
|
jose: 4.6.0
|
||||||
|
@ -2375,10 +2365,10 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.19.1
|
zod: 3.19.1
|
||||||
|
|
||||||
/@logto/node/1.0.0-beta.10:
|
/@logto/node/1.0.0-beta.11:
|
||||||
resolution: {integrity: sha512-4st77cD1h/bCIrt+BttDHva/I7ibLw0ilvi30uadEwuH0Ih8WFYvlyi2V1wOljd5Ym+fqSMbyKXQQIwba6CenQ==}
|
resolution: {integrity: sha512-nsa9RtrzBRmLMJNeNzutJ72bLwZpGUr4lhBAze7YXJi6o9ujvZOLgdvZAZeoSdxxBhTilwbDWNXmj8n3Ndavaw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/client': 1.0.0-beta.10
|
'@logto/client': 1.0.0-beta.11
|
||||||
'@silverhand/essentials': 1.3.0
|
'@silverhand/essentials': 1.3.0
|
||||||
js-base64: 3.7.2
|
js-base64: 3.7.2
|
||||||
node-fetch: 2.6.7
|
node-fetch: 2.6.7
|
||||||
|
@ -2386,12 +2376,12 @@ packages:
|
||||||
- encoding
|
- encoding
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/react/1.0.0-beta.10_react@18.2.0:
|
/@logto/react/1.0.0-beta.11_react@18.2.0:
|
||||||
resolution: {integrity: sha512-MutQplD5VkUqYIhnaQgNiRpbQTRK/OS2Ut70j99mVax/frImlIu1m1YeuE8NEqGmB9SBQvJoP5kECxVNf2a4Nw==}
|
resolution: {integrity: sha512-W9L1QrJml4tHlrUkmdaJRsj4ln+SgwGAeyF+QhmKBq5CoSR+EauE63ySLhPn2KFVrc3m/sioejJIVy+1sUe21w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16.8.0 || ^18.0.0'
|
react: '>=16.8.0 || ^18.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/browser': 1.0.0-beta.10
|
'@logto/browser': 1.0.0-beta.11
|
||||||
'@silverhand/essentials': 1.3.0
|
'@silverhand/essentials': 1.3.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
Loading…
Reference in a new issue