mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
Merge branch master into merge/sie-v2
This commit is contained in:
commit
5c81d6f147
81 changed files with 1799 additions and 661 deletions
|
@ -12,13 +12,6 @@
|
|||
"@logto/integration-tests",
|
||||
"@logto/ui"
|
||||
]],
|
||||
"//": "Ignore other release group members, only keep the major one.",
|
||||
"ignore": [
|
||||
"@logto/create",
|
||||
"@logto/console",
|
||||
"@logto/integration-tests",
|
||||
"@logto/ui"
|
||||
],
|
||||
"linked": [[
|
||||
"@logto/phrases",
|
||||
"@logto/phrases-ui",
|
||||
|
|
6
.changeset/fifty-balloons-taste.md
Normal file
6
.changeset/fifty-balloons-taste.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@logto/phrases": minor
|
||||
"@logto/phrases-ui": minor
|
||||
---
|
||||
|
||||
Add German language
|
18
.changeset/pre.json
Normal file
18
.changeset/pre.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"mode": "pre",
|
||||
"tag": "beta",
|
||||
"initialVersions": {
|
||||
"@logto/cli": "1.0.0-beta.12",
|
||||
"@logto/console": "1.0.0-beta.12",
|
||||
"@logto/core": "1.0.0-beta.12",
|
||||
"@logto/create": "1.0.0-beta.12",
|
||||
"@logto/demo-app": "1.0.0-beta.12",
|
||||
"@logto/integration-tests": "1.0.0-beta.12",
|
||||
"@logto/phrases": "1.0.0-beta.12",
|
||||
"@logto/phrases-ui": "1.0.0-beta.12",
|
||||
"@logto/schemas": "1.0.0-beta.12",
|
||||
"@logto/shared": "1.0.0-beta.12",
|
||||
"@logto/ui": "1.0.0-beta.12"
|
||||
},
|
||||
"changesets": []
|
||||
}
|
8
.github/workflows/integration-test.yml
vendored
8
.github/workflows/integration-test.yml
vendored
|
@ -38,9 +38,9 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node_version: [16, 18]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -55,6 +55,7 @@ jobs:
|
|||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
run-install: false
|
||||
|
||||
# Setup integration test
|
||||
|
@ -63,6 +64,9 @@ jobs:
|
|||
cd tests
|
||||
pnpm i
|
||||
pnpm prepack
|
||||
# Install Chromium
|
||||
cd packages/integration-tests/node_modules/puppeteer
|
||||
pnpm postinstall
|
||||
|
||||
# Setup environment
|
||||
- name: Setup Postgres
|
||||
|
|
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
|
@ -47,11 +47,17 @@ jobs:
|
|||
main-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node_version: [16, 18]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
|
||||
- name: Prepack
|
||||
run: pnpm prepack
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
FROM node:16-alpine as builder
|
||||
WORKDIR /etc/logto
|
||||
ENV CI=true
|
||||
COPY . .
|
||||
|
||||
# Install toolchain
|
||||
RUN npm add --location=global pnpm@^7.2.1
|
||||
RUN npm add --location=global pnpm@^7.14.0
|
||||
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#node-gyp-alpine
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install dependencies and build
|
||||
RUN pnpm i
|
||||
RUN pnpm -r build
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.0",
|
||||
"pnpm": ">=6"
|
||||
"node": "^16.13.0 || ^18.12.0",
|
||||
"pnpm": "^7.14.0"
|
||||
},
|
||||
"alias": {
|
||||
"html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"prepack": "pnpm build"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/logto-io/logto/issues"
|
||||
|
@ -47,7 +47,7 @@
|
|||
"decamelize": "^5.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"got": "^11.8.2",
|
||||
"got": "^11.8.5",
|
||||
"hpagent": "^1.0.0",
|
||||
"inquirer": "^8.2.2",
|
||||
"nanoid": "^3.3.4",
|
||||
|
|
|
@ -27,16 +27,17 @@ export const defaultPath = path.join(os.homedir(), 'logto');
|
|||
const pgRequired = new semver.SemVer('14.0.0');
|
||||
|
||||
export const validateNodeVersion = () => {
|
||||
const required = new semver.SemVer('16.0.0');
|
||||
const required = [new semver.SemVer('16.13.0'), new semver.SemVer('18.12.0')];
|
||||
const requiredVersionString = required.map((version) => '^' + version.version).join(' || ');
|
||||
const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' }));
|
||||
|
||||
if (required.compare(current) > 0) {
|
||||
log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`);
|
||||
if (required.every((version) => version.major !== current.major)) {
|
||||
log.error(`Logto requires NodeJS ${requiredVersionString}, but ${current.version} found.`);
|
||||
}
|
||||
|
||||
if (current.major > required.major) {
|
||||
if (required.some((version) => version.major === current.major && version.compare(current) > 0)) {
|
||||
log.warn(
|
||||
`Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.`
|
||||
`Logto is tested under NodeJS ${requiredVersionString}, but version ${current.version} found.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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.11",
|
||||
"@logto/react": "1.0.0-beta.12",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@parcel/core": "2.7.0",
|
||||
|
@ -50,6 +50,7 @@
|
|||
"csstype": "^3.0.11",
|
||||
"dayjs": "^1.10.5",
|
||||
"deep-object-diff": "^1.1.7",
|
||||
"date-fns": "^2.29.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dnd-core": "^16.0.0",
|
||||
"eslint": "^8.21.0",
|
||||
|
@ -88,6 +89,17 @@
|
|||
"typescript": "^4.7.4",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"//": "https://github.com/parcel-bundler/parcel/issues/7636",
|
||||
"targets": {
|
||||
"default": {
|
||||
"engines": {
|
||||
"browsers": "defaults"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alias": {
|
||||
"@/*": "./src/$1",
|
||||
"@mdx/components/*": "./src/mdx-components/$1"
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB |
|
@ -1,18 +1,18 @@
|
|||
import type { Nullable } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { isValid } from 'date-fns';
|
||||
|
||||
type Props = {
|
||||
children: Nullable<string | number>;
|
||||
};
|
||||
|
||||
const DateTime = ({ children }: Props) => {
|
||||
const date = dayjs(children);
|
||||
const date = children && new Date(children);
|
||||
|
||||
if (!children || !date.isValid()) {
|
||||
if (!date || !isValid(date)) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
return <span>{date.toDate().toLocaleDateString()}</span>;
|
||||
return <span>{date.toLocaleDateString()}</span>;
|
||||
};
|
||||
|
||||
export default DateTime;
|
||||
|
|
|
@ -7,7 +7,6 @@ import avatar006 from '@/assets/avatars/avatar-006.png';
|
|||
import avatar007 from '@/assets/avatars/avatar-007.png';
|
||||
import avatar008 from '@/assets/avatars/avatar-008.png';
|
||||
import avatar009 from '@/assets/avatars/avatar-009.png';
|
||||
import avatar010 from '@/assets/avatars/avatar-010.png';
|
||||
|
||||
export const Avatars = [
|
||||
avatar001,
|
||||
|
@ -19,7 +18,6 @@ export const Avatars = [
|
|||
avatar007,
|
||||
avatar008,
|
||||
avatar009,
|
||||
avatar010,
|
||||
];
|
||||
|
||||
export const generateAvatarPlaceHolderById = (id: string) =>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { LogDto, User } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
@ -85,7 +84,7 @@ const AuditLogDetails = () => {
|
|||
</div>
|
||||
<div className={styles.infoItem}>
|
||||
<div className={styles.label}>{t('log_details.time')}</div>
|
||||
<div>{dayjs(data.createdAt).toDate().toLocaleString()}</div>
|
||||
<div>{new Date(data.createdAt).toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { format } from 'date-fns';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -36,7 +36,7 @@ const tickFormatter = new Intl.NumberFormat('en-US', {
|
|||
});
|
||||
|
||||
const Dashboard = () => {
|
||||
const [date, setDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||
const [date, setDate] = useState<string>(format(Date.now(), 'yyyy-MM-dd'));
|
||||
const { data: totalData, error: totalError } = useSWR<TotalUsersResponse, RequestError>(
|
||||
'/api/dashboard/users/total'
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { ConnectorResponse, ConnectorMetadata, SignInExperience } from '@lo
|
|||
import { AppearanceMode } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import { format } from 'date-fns';
|
||||
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
@ -195,7 +195,7 @@ const Preview = ({ signInExperience, className }: Props) => {
|
|||
<div className={classNames(styles.device, styles[mode])}>
|
||||
{platform !== 'desktopWeb' && (
|
||||
<div className={styles.topBar}>
|
||||
<div className={styles.time}>{dayjs().format('HH:mm')}</div>
|
||||
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
|
||||
<PhoneInfo />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"@silverhand/essentials": "^1.3.0",
|
||||
"chalk": "^4",
|
||||
"clean-deep": "^3.4.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"debug": "^4.3.4",
|
||||
"decamelize": "^5.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
|
@ -38,7 +38,7 @@
|
|||
"etag": "^1.8.1",
|
||||
"find-up": "^5.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"got": "^11.8.2",
|
||||
"got": "^11.8.5",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"i18next": "^21.8.16",
|
||||
"iconv-lite": "0.6.3",
|
||||
|
@ -55,7 +55,7 @@
|
|||
"lodash.pick": "^4.4.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"nanoid": "^3.1.23",
|
||||
"oidc-provider": "^7.11.3",
|
||||
"oidc-provider": "^7.13.0",
|
||||
"p-retry": "^4.6.1",
|
||||
"query-string": "^7.0.1",
|
||||
"roarr": "^7.11.0",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"@types/koa-send": "^4.1.3",
|
||||
"@types/lodash.pick": "^4.4.6",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/oidc-provider": "^7.11.1",
|
||||
"@types/oidc-provider": "^7.12.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.21.0",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@": "./build"
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export const isTrue = (value: string) =>
|
||||
['1', 'true', 'y', 'yes', 'yep', 'yeah'].includes(value.toLowerCase());
|
||||
export const isTrue = (value?: string) =>
|
||||
// We need to leverage the native type guard
|
||||
// eslint-disable-next-line no-implicit-coercion
|
||||
!!value && ['1', 'true', 'y', 'yes', 'yep', 'yeah'].includes(value.toLowerCase());
|
||||
|
|
|
@ -3,7 +3,7 @@ import { has } from '@silverhand/essentials';
|
|||
import type { MiddlewareType } from 'koa';
|
||||
import koaBody from 'koa-body';
|
||||
import type { IMiddleware, IRouterParamContext } from 'koa-router';
|
||||
import type { ZodType } from 'zod';
|
||||
import type { ZodType, ZodTypeDef } from 'zod';
|
||||
|
||||
import envSet from '@/env-set';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
@ -48,7 +48,7 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
|
|||
): function_ is WithGuardConfig<Type> =>
|
||||
function_.name === 'guardMiddleware' && has(function_, 'config');
|
||||
|
||||
const tryParse = <Output, Definition, Input>(
|
||||
const tryParse = <Output, Definition extends ZodTypeDef, Input>(
|
||||
type: 'query' | 'body' | 'params',
|
||||
guard: Optional<ZodType<Output, Definition, Input>>,
|
||||
data: unknown
|
||||
|
|
|
@ -35,10 +35,9 @@ jest.mock('@logto/shared', () => ({
|
|||
const now = Date.now();
|
||||
|
||||
jest.mock(
|
||||
'dayjs',
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
jest.fn(() => () => ({
|
||||
add: jest.fn((delta: number) => new Date(now + delta * 1000)),
|
||||
'date-fns',
|
||||
jest.fn(() => ({
|
||||
addSeconds: jest.fn((_: Date, seconds: number) => new Date(now + seconds * 1000)),
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { CreateApplication, OidcClientMetadata } from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import dayjs from 'dayjs';
|
||||
import { addSeconds } from 'date-fns';
|
||||
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
|
@ -99,7 +99,7 @@ export default function postgresAdapter(modelName: string): ReturnType<AdapterFa
|
|||
modelName,
|
||||
id,
|
||||
payload,
|
||||
expiresAt: dayjs().add(expiresIn, 'second').valueOf(),
|
||||
expiresAt: addSeconds(Date.now(), expiresIn).valueOf(),
|
||||
}),
|
||||
find: async (id) => findPayloadById(modelName, id),
|
||||
findByUserCode: async (userCode) => findPayloadByPayloadField(modelName, 'userCode', userCode),
|
||||
|
|
|
@ -7,7 +7,7 @@ import { OidcModelInstances } from '@logto/schemas';
|
|||
import { convertToIdentifiers, convertToTimestamp } from '@logto/shared';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { addSeconds, isBefore } from 'date-fns';
|
||||
import type { ValueExpression } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
|
@ -30,7 +30,7 @@ const isConsumed = (modelName: string, consumedAt: Nullable<number>): boolean =>
|
|||
return Boolean(consumedAt);
|
||||
}
|
||||
|
||||
return dayjs(consumedAt).add(refreshTokenReuseInterval, 'seconds').isBefore(dayjs());
|
||||
return isBefore(addSeconds(consumedAt, refreshTokenReuseInterval), Date.now());
|
||||
};
|
||||
|
||||
const withConsumed = <T>(
|
||||
|
|
|
@ -240,7 +240,7 @@ describe('user query', () => {
|
|||
const expectSql = sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${fields.username} like $3 or ${fields.name} like $4
|
||||
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${fields.username} ilike $3 or ${fields.name} ilike $4
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
|
@ -259,7 +259,7 @@ describe('user query', () => {
|
|||
select count(*)
|
||||
from ${table}
|
||||
where not (${fields.roleNames}::jsonb?$1)
|
||||
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${fields.username} like $4 or ${fields.name} like $5)
|
||||
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${fields.username} ilike $4 or ${fields.name} ilike $5)
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
|
@ -278,6 +278,24 @@ describe('user query', () => {
|
|||
await expect(countUsers(search, true)).resolves.toEqual(dbvalue);
|
||||
});
|
||||
|
||||
it('countUsers with isCaseSensitive', async () => {
|
||||
const search = 'foo';
|
||||
const expectSql = sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${fields.username} like $3 or ${fields.name} like $4
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`]);
|
||||
|
||||
return createMockQueryResult([dbvalue]);
|
||||
});
|
||||
|
||||
await expect(countUsers(search, undefined, true)).resolves.toEqual(dbvalue);
|
||||
});
|
||||
|
||||
it('findUsers', async () => {
|
||||
const search = 'foo';
|
||||
const limit = 100;
|
||||
|
@ -285,9 +303,9 @@ describe('user query', () => {
|
|||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
|
||||
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${
|
||||
fields.username
|
||||
} like $3 or ${fields.name} like $4
|
||||
} ilike $3 or ${fields.name} ilike $4
|
||||
limit $5
|
||||
offset $6
|
||||
`;
|
||||
|
@ -317,9 +335,9 @@ describe('user query', () => {
|
|||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where not (${fields.roleNames}::jsonb?$1)
|
||||
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${
|
||||
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${
|
||||
fields.username
|
||||
} like $4 or ${fields.name} like $5)
|
||||
} ilike $4 or ${fields.name} ilike $5)
|
||||
limit $6
|
||||
offset $7
|
||||
`;
|
||||
|
@ -342,6 +360,37 @@ describe('user query', () => {
|
|||
await expect(findUsers(limit, offset, search, true)).resolves.toEqual([dbvalue]);
|
||||
});
|
||||
|
||||
it('findUsers with isCaseSensitive', async () => {
|
||||
const search = 'foo';
|
||||
const limit = 100;
|
||||
const offset = 1;
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
|
||||
fields.username
|
||||
} like $3 or ${fields.name} like $4
|
||||
limit $5
|
||||
offset $6
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
limit,
|
||||
offset,
|
||||
]);
|
||||
|
||||
return createMockQueryResult([dbvalue]);
|
||||
});
|
||||
|
||||
await expect(findUsers(limit, offset, search, undefined, true)).resolves.toEqual([dbvalue]);
|
||||
});
|
||||
|
||||
it('updateUserById', async () => {
|
||||
const username = 'Joe';
|
||||
const id = 'foo';
|
||||
|
|
|
@ -84,44 +84,64 @@ export const hasUserWithIdentity = async (target: string, userId: string) =>
|
|||
`
|
||||
);
|
||||
|
||||
const buildUserSearchConditionSql = (search: string) => {
|
||||
const buildUserSearchConditionSql = (search: string, isCaseSensitive = false) => {
|
||||
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
|
||||
const conditions = searchFields.map((filedName) => sql`${filedName} like ${'%' + search + '%'}`);
|
||||
|
||||
return sql`${sql.join(conditions, sql` or `)}`;
|
||||
return sql`${sql.join(
|
||||
searchFields.map(
|
||||
(filedName) =>
|
||||
sql`${filedName} ${isCaseSensitive ? sql`like` : sql`ilike`} ${'%' + search + '%'}`
|
||||
),
|
||||
sql` or `
|
||||
)}`;
|
||||
};
|
||||
|
||||
const buildUserConditions = (search?: string, hideAdminUser?: boolean) => {
|
||||
const buildUserConditions = (
|
||||
search?: string,
|
||||
hideAdminUser?: boolean,
|
||||
isCaseSensitive?: boolean
|
||||
) => {
|
||||
if (hideAdminUser) {
|
||||
return sql`
|
||||
where not (${fields.roleNames}::jsonb?${UserRole.Admin})
|
||||
${conditionalSql(search, (search) => sql`and (${buildUserSearchConditionSql(search)})`)}
|
||||
${conditionalSql(
|
||||
search,
|
||||
(search) => sql`and (${buildUserSearchConditionSql(search, isCaseSensitive)})`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
return sql`
|
||||
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
|
||||
${conditionalSql(
|
||||
search,
|
||||
(search) => sql`where ${buildUserSearchConditionSql(search, isCaseSensitive)}`
|
||||
)}
|
||||
`;
|
||||
};
|
||||
|
||||
export const countUsers = async (search?: string, hideAdminUser?: boolean) =>
|
||||
export const countUsers = async (
|
||||
search?: string,
|
||||
hideAdminUser?: boolean,
|
||||
isCaseSensitive?: boolean
|
||||
) =>
|
||||
envSet.pool.one<{ count: number }>(sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
${buildUserConditions(search, hideAdminUser)}
|
||||
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
|
||||
`);
|
||||
|
||||
export const findUsers = async (
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: string,
|
||||
hideAdminUser?: boolean
|
||||
hideAdminUser?: boolean,
|
||||
isCaseSensitive?: boolean
|
||||
) =>
|
||||
envSet.pool.any<User>(
|
||||
sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
${buildUserConditions(search, hideAdminUser)}
|
||||
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`
|
||||
|
|
|
@ -4,6 +4,7 @@ import { has } from '@silverhand/essentials';
|
|||
import pick from 'lodash.pick';
|
||||
import { literal, object, string } from 'zod';
|
||||
|
||||
import { isTrue } from '@/env-set/parameters';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { encryptUserPassword, generateUserId, insertUser } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
|
@ -27,18 +28,24 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
'/users',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
query: object({ search: string().optional(), hideAdminUser: literal('true').optional() }),
|
||||
query: object({
|
||||
search: string().optional(),
|
||||
// Use `.transform()` once the type issue fixed
|
||||
hideAdminUser: string().optional(),
|
||||
isCaseSensitive: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const {
|
||||
query: { search, hideAdminUser: _hideAdminUser },
|
||||
query: { search, hideAdminUser: _hideAdminUser, isCaseSensitive: _isCaseSensitive },
|
||||
} = ctx.guard;
|
||||
|
||||
const hideAdminUser = _hideAdminUser === 'true';
|
||||
const hideAdminUser = isTrue(_hideAdminUser);
|
||||
const isCaseSensitive = isTrue(_isCaseSensitive);
|
||||
const [{ count }, users] = await Promise.all([
|
||||
countUsers(search, hideAdminUser),
|
||||
findUsers(limit, offset, search, hideAdminUser),
|
||||
countUsers(search, hideAdminUser, isCaseSensitive),
|
||||
findUsers(limit, offset, search, hideAdminUser, isCaseSensitive),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
|
|
|
@ -40,13 +40,13 @@ jest.mock('@/queries/custom-phrase', () => ({
|
|||
upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase),
|
||||
}));
|
||||
|
||||
const isValidStructure = jest.fn(
|
||||
const isStrictlyPartial = jest.fn(
|
||||
(fullTranslation: Translation, partialTranslation: Partial<Translation>) => true
|
||||
);
|
||||
|
||||
jest.mock('@/utils/translation', () => ({
|
||||
isValidStructure: (fullTranslation: Translation, partialTranslation: Translation) =>
|
||||
isValidStructure(fullTranslation, partialTranslation),
|
||||
isStrictlyPartial: (fullTranslation: Translation, partialTranslation: Translation) =>
|
||||
isStrictlyPartial(fullTranslation, partialTranslation),
|
||||
}));
|
||||
|
||||
const mockFallbackLanguage = trTrTag;
|
||||
|
@ -130,13 +130,13 @@ describe('customPhraseRoutes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should call isValidStructure', async () => {
|
||||
it('should call isStrictlyPartial', async () => {
|
||||
await customPhraseRequest.put(`/custom-phrases/${mockLanguageTag}`).send(translation);
|
||||
expect(isValidStructure).toBeCalledWith(en.translation, translation);
|
||||
expect(isStrictlyPartial).toBeCalledWith(en.translation, translation);
|
||||
});
|
||||
|
||||
it('should fail when the input translation structure is invalid', async () => {
|
||||
isValidStructure.mockReturnValueOnce(false);
|
||||
isStrictlyPartial.mockReturnValueOnce(false);
|
||||
const response = await customPhraseRequest
|
||||
.put(`/custom-phrases/${mockLanguageTag}`)
|
||||
.send(translation);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from '@/queries/custom-phrase';
|
||||
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { isValidStructure } from '@/utils/translation';
|
||||
import { isStrictlyPartial } from '@/utils/translation';
|
||||
|
||||
import type { AuthedRouter } from './types';
|
||||
|
||||
|
@ -70,7 +70,7 @@ export default function customPhraseRoutes<T extends AuthedRouter>(router: T) {
|
|||
const translation = cleanDeepTranslation(body);
|
||||
|
||||
assertThat(
|
||||
isValidStructure(resource.en.translation, translation),
|
||||
isStrictlyPartial(resource.en.translation, translation),
|
||||
new RequestError('localization.invalid_translation_structure')
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import dayjs from 'dayjs';
|
||||
// The FP version works better for `format()`
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import { endOfDay, subDays } from 'date-fns';
|
||||
import { format } from 'date-fns/fp';
|
||||
/* eslint-enable import/no-duplicates */
|
||||
|
||||
import dashboardRoutes from '@/routes/dashboard';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
@ -8,6 +12,7 @@ const countUsers = jest.fn(async () => ({ count: totalUserCount }));
|
|||
const getDailyNewUserCountsByTimeInterval = jest.fn(
|
||||
async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyNewUserCounts
|
||||
);
|
||||
const formatToQueryDate = format('yyyy-MM-dd');
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
countUsers: async () => countUsers(),
|
||||
|
@ -83,8 +88,8 @@ describe('dashboardRoutes', () => {
|
|||
it('should call getDailyNewUserCountsByTimeInterval with the time interval (14 days ago 23:59:59.999, today 23:59:59.999]', async () => {
|
||||
await logRequest.get('/dashboard/users/new');
|
||||
expect(getDailyNewUserCountsByTimeInterval).toHaveBeenCalledWith(
|
||||
dayjs().endOf('day').subtract(14, 'day').valueOf(),
|
||||
dayjs().endOf('day').valueOf()
|
||||
subDays(endOfDay(Date.now()), 14).valueOf(),
|
||||
endOfDay(Date.now()).valueOf()
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -105,10 +110,10 @@ describe('dashboardRoutes', () => {
|
|||
});
|
||||
|
||||
describe('GET /dashboard/users/active', () => {
|
||||
const mockToday = '2022-05-30';
|
||||
const mockToday = new Date(2022, 4, 30);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(mockToday));
|
||||
jest.useFakeTimers().setSystemTime(mockToday);
|
||||
});
|
||||
|
||||
it('should fail when the parameter `date` does not match the date regex', async () => {
|
||||
|
@ -117,44 +122,44 @@ describe('dashboardRoutes', () => {
|
|||
});
|
||||
|
||||
it('should call getDailyActiveUserCountsByTimeInterval with the time interval (2022-05-31, 2022-06-30] when the parameter `date` is 2022-06-30', async () => {
|
||||
const targetDate = '2022-06-30';
|
||||
await logRequest.get(`/dashboard/users/active?date=${targetDate}`);
|
||||
const targetDate = new Date(2022, 5, 30);
|
||||
await logRequest.get(`/dashboard/users/active?date=${formatToQueryDate(targetDate)}`);
|
||||
expect(getDailyActiveUserCountsByTimeInterval).toHaveBeenCalledWith(
|
||||
dayjs('2022-05-31').endOf('day').valueOf(),
|
||||
dayjs(targetDate).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 4, 31)).valueOf(),
|
||||
endOfDay(targetDate).valueOf()
|
||||
);
|
||||
});
|
||||
|
||||
it('should call getDailyActiveUserCountsByTimeInterval with the time interval (30 days ago, tomorrow] when there is no parameter `date`', async () => {
|
||||
await logRequest.get('/dashboard/users/active');
|
||||
expect(getDailyActiveUserCountsByTimeInterval).toHaveBeenCalledWith(
|
||||
dayjs('2022-04-30').endOf('day').valueOf(),
|
||||
dayjs(mockToday).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 3, 30)).valueOf(),
|
||||
endOfDay(mockToday).valueOf()
|
||||
);
|
||||
});
|
||||
|
||||
it('should call countActiveUsersByTimeInterval with correct parameters when the parameter `date` is 2022-06-30', async () => {
|
||||
const targetDate = '2022-06-30';
|
||||
await logRequest.get(`/dashboard/users/active?date=${targetDate}`);
|
||||
const targetDate = new Date(2022, 5, 30);
|
||||
await logRequest.get(`/dashboard/users/active?date=${formatToQueryDate(targetDate)}`);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
dayjs('2022-06-16').endOf('day').valueOf(),
|
||||
dayjs('2022-06-23').endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 5, 16)).valueOf(),
|
||||
endOfDay(new Date(2022, 5, 23)).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
dayjs('2022-06-23').endOf('day').valueOf(),
|
||||
dayjs(targetDate).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 5, 23)).valueOf(),
|
||||
endOfDay(targetDate).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
dayjs('2022-05-01').endOf('day').valueOf(),
|
||||
dayjs('2022-05-31').endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 4, 1)).valueOf(),
|
||||
endOfDay(new Date(2022, 4, 31)).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
dayjs('2022-05-31').endOf('day').valueOf(),
|
||||
dayjs(targetDate).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 4, 31)).valueOf(),
|
||||
endOfDay(targetDate).valueOf()
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -162,23 +167,23 @@ describe('dashboardRoutes', () => {
|
|||
await logRequest.get('/dashboard/users/active');
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
dayjs('2022-05-16').endOf('day').valueOf(),
|
||||
dayjs('2022-05-23').endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 4, 16)).valueOf(),
|
||||
endOfDay(new Date(2022, 4, 23)).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
dayjs('2022-05-23').endOf('day').valueOf(),
|
||||
dayjs(mockToday).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 4, 23)).valueOf(),
|
||||
endOfDay(mockToday).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
dayjs('2022-03-31').endOf('day').valueOf(),
|
||||
dayjs('2022-04-30').endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 2, 31)).valueOf(),
|
||||
endOfDay(new Date(2022, 3, 30)).valueOf()
|
||||
);
|
||||
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
dayjs('2022-04-30').endOf('day').valueOf(),
|
||||
dayjs(mockToday).endOf('day').valueOf()
|
||||
endOfDay(new Date(2022, 3, 30)).valueOf(),
|
||||
endOfDay(mockToday).valueOf()
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { dateRegex } from '@logto/core-kit';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { endOfDay, format, subDays } from 'date-fns';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
|
@ -12,11 +11,11 @@ import { countUsers, getDailyNewUserCountsByTimeInterval } from '@/queries/user'
|
|||
|
||||
import type { AuthedRouter } from './types';
|
||||
|
||||
const getDateString = (day: Dayjs) => day.format('YYYY-MM-DD');
|
||||
const getDateString = (date: Date | number) => format(date, 'yyyy-MM-dd');
|
||||
|
||||
const indices = (length: number) => [...Array.from({ length }).keys()];
|
||||
|
||||
const lastTimestampOfDay = (day: Dayjs) => day.endOf('day').valueOf();
|
||||
const getEndOfDayTimestamp = (date: Date | number) => endOfDay(date).valueOf();
|
||||
|
||||
export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
|
||||
router.get('/dashboard/users/total', async (ctx, next) => {
|
||||
|
@ -27,11 +26,11 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
|
|||
});
|
||||
|
||||
router.get('/dashboard/users/new', async (ctx, next) => {
|
||||
const today = dayjs();
|
||||
const today = Date.now();
|
||||
const dailyNewUserCounts = await getDailyNewUserCountsByTimeInterval(
|
||||
// (14 days ago 23:59:59.999, today 23:59:59.999]
|
||||
lastTimestampOfDay(today.subtract(14, 'day')),
|
||||
lastTimestampOfDay(today)
|
||||
getEndOfDayTimestamp(subDays(today, 14)),
|
||||
getEndOfDayTimestamp(today)
|
||||
);
|
||||
|
||||
const last14DaysNewUserCounts = new Map(
|
||||
|
@ -39,15 +38,15 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
|
|||
);
|
||||
|
||||
const todayNewUserCount = last14DaysNewUserCounts.get(getDateString(today)) ?? 0;
|
||||
const yesterday = today.subtract(1, 'day');
|
||||
const yesterday = subDays(today, 1);
|
||||
const yesterdayNewUserCount = last14DaysNewUserCounts.get(getDateString(yesterday)) ?? 0;
|
||||
const todayDelta = todayNewUserCount - yesterdayNewUserCount;
|
||||
|
||||
const last7DaysNewUserCount = indices(7)
|
||||
.map((index) => getDateString(today.subtract(index, 'day')))
|
||||
.map((index) => getDateString(subDays(today, index)))
|
||||
.reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0);
|
||||
const newUserCountFrom13DaysAgoTo7DaysAgo = indices(7)
|
||||
.map((index) => getDateString(today.subtract(7 + index, 'day')))
|
||||
.map((index) => getDateString(subDays(today, index + 7)))
|
||||
.reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0);
|
||||
const last7DaysDelta = last7DaysNewUserCount - newUserCountFrom13DaysAgoTo7DaysAgo;
|
||||
|
||||
|
@ -75,7 +74,7 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
|
|||
query: { date },
|
||||
} = ctx.guard;
|
||||
|
||||
const targetDay = date ? dayjs(date) : dayjs(); // Defaults to today
|
||||
const targetDay = date ? new Date(date) : new Date(); // Defaults to today
|
||||
const [
|
||||
// DAU: Daily Active User
|
||||
last30DauCounts,
|
||||
|
@ -88,39 +87,39 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
|
|||
] = await Promise.all([
|
||||
getDailyActiveUserCountsByTimeInterval(
|
||||
// (30 days ago 23:59:59.999, target day 23:59:59.999]
|
||||
lastTimestampOfDay(targetDay.subtract(30, 'day')),
|
||||
lastTimestampOfDay(targetDay)
|
||||
getEndOfDayTimestamp(subDays(targetDay, 30)),
|
||||
getEndOfDayTimestamp(targetDay)
|
||||
),
|
||||
countActiveUsersByTimeInterval(
|
||||
// (14 days ago 23:59:59.999, 7 days ago 23:59:59.999]
|
||||
lastTimestampOfDay(targetDay.subtract(14, 'day')),
|
||||
lastTimestampOfDay(targetDay.subtract(7, 'day'))
|
||||
getEndOfDayTimestamp(subDays(targetDay, 14)),
|
||||
getEndOfDayTimestamp(subDays(targetDay, 7))
|
||||
),
|
||||
countActiveUsersByTimeInterval(
|
||||
// (7 days ago 23:59:59.999, target day 23:59:59.999]
|
||||
lastTimestampOfDay(targetDay.subtract(7, 'day')),
|
||||
lastTimestampOfDay(targetDay)
|
||||
getEndOfDayTimestamp(subDays(targetDay, 7)),
|
||||
getEndOfDayTimestamp(targetDay)
|
||||
),
|
||||
countActiveUsersByTimeInterval(
|
||||
// (60 days ago 23:59:59.999, 30 days ago 23:59:59.999]
|
||||
lastTimestampOfDay(targetDay.subtract(60, 'day')),
|
||||
lastTimestampOfDay(targetDay.subtract(30, 'day'))
|
||||
getEndOfDayTimestamp(subDays(targetDay, 60)),
|
||||
getEndOfDayTimestamp(subDays(targetDay, 30))
|
||||
),
|
||||
countActiveUsersByTimeInterval(
|
||||
// (30 days ago 23:59:59.999, target day 23:59:59.999]
|
||||
lastTimestampOfDay(targetDay.subtract(30, 'day')),
|
||||
lastTimestampOfDay(targetDay)
|
||||
getEndOfDayTimestamp(subDays(targetDay, 30)),
|
||||
getEndOfDayTimestamp(targetDay)
|
||||
),
|
||||
]);
|
||||
|
||||
const previousDate = getDateString(targetDay.subtract(1, 'day'));
|
||||
const previousDate = getDateString(subDays(targetDay, 1));
|
||||
const targetDate = getDateString(targetDay);
|
||||
|
||||
const previousDAU = last30DauCounts.find(({ date }) => date === previousDate)?.count ?? 0;
|
||||
const dau = last30DauCounts.find(({ date }) => date === targetDate)?.count ?? 0;
|
||||
|
||||
const dauCurve = indices(30).map((index) => {
|
||||
const dateString = getDateString(targetDay.subtract(29 - index, 'day'));
|
||||
const dateString = getDateString(subDays(targetDay, 29 - index));
|
||||
const count = last30DauCounts.find(({ date }) => date === dateString)?.count ?? 0;
|
||||
|
||||
return { date: dateString, count };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { addDays, subSeconds } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockUser } from '@/__mocks__';
|
||||
|
@ -6,6 +6,7 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import continueRoutes, { continueRoute } from './continue';
|
||||
|
||||
const getTomorrowIsoString = () => addDays(Date.now(), 1).toISOString();
|
||||
const getVerificationStorageFromInteraction = jest.fn();
|
||||
|
||||
const checkRequiredProfile = jest.fn();
|
||||
|
@ -68,7 +69,7 @@ describe('session -> continueRoutes', () => {
|
|||
result: {
|
||||
continueSignIn: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -100,7 +101,7 @@ describe('session -> continueRoutes', () => {
|
|||
result: {
|
||||
continueSignIn: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -136,7 +137,7 @@ describe('session -> continueRoutes', () => {
|
|||
result: {
|
||||
continueSignIn: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -167,7 +168,7 @@ describe('session -> continueRoutes', () => {
|
|||
result: {
|
||||
continueSignIn: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -214,7 +215,7 @@ describe('session -> continueRoutes', () => {
|
|||
result: {
|
||||
continueSignIn: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs().subtract(1, 'second').toISOString(),
|
||||
expiresAt: subSeconds(Date.now(), 1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockPasswordEncrypted, mockSignInExperience, mockUserWithPassword } from '@/__mocks__';
|
||||
|
@ -16,6 +16,8 @@ const encryptUserPassword = jest.fn(async (password: string) => ({
|
|||
const findUserById = jest.fn(async (): Promise<User> => mockUserWithPassword);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ userId: 'id' }));
|
||||
const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience);
|
||||
const getYesterdayDate = () => subDays(Date.now(), 1);
|
||||
const getTomorrowDate = () => addDays(Date.now(), 1);
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
...jest.requireActual('@/lib/user'),
|
||||
|
@ -89,7 +91,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
userId: 'id',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowDate().toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
},
|
||||
|
@ -110,7 +112,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowDate().toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
},
|
||||
|
@ -126,7 +128,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
userId: 'id',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowDate().toISOString(),
|
||||
flow: PasscodeType.SignIn,
|
||||
},
|
||||
},
|
||||
|
@ -170,7 +172,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
userId: 'id',
|
||||
expiresAt: dayjs().subtract(1, 'day').toISOString(),
|
||||
expiresAt: getYesterdayDate().toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
},
|
||||
|
@ -186,7 +188,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
userId: 'id',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowDate().toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
},
|
||||
|
@ -203,7 +205,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
userId: 'id',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowDate().toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
import type { User } from '@logto/schemas';
|
||||
import { PasscodeType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { addDays, addSeconds, subDays } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockSignInExperience, mockSignInMethod, mockUser } from '@/__mocks__';
|
||||
|
@ -25,6 +25,7 @@ const findDefaultSignInExperience = jest.fn(async () => ({
|
|||
verify: true,
|
||||
},
|
||||
}));
|
||||
const getTomorrowIsoString = () => addDays(Date.now(), 1).toISOString();
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
|
@ -203,7 +204,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
flow: PasscodeType.SignIn,
|
||||
phone: '13000000000',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -227,7 +228,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
phone: '13000000000',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -251,7 +252,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.objectContaining({
|
||||
verification: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
})
|
||||
|
@ -300,7 +301,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
flow: PasscodeType.SignIn,
|
||||
email: 'a@a.com',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -323,7 +324,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
email: 'a@a.com',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -347,7 +348,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.objectContaining({
|
||||
verification: {
|
||||
userId: mockUser.id,
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
},
|
||||
})
|
||||
|
@ -379,7 +380,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -403,7 +404,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -424,7 +425,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
phone: '13000000000',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -438,7 +439,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -466,7 +467,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().subtract(1, 'day').toISOString(),
|
||||
expiresAt: subDays(Date.now(), 1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -480,7 +481,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'XX@foo',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -494,7 +495,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -542,7 +543,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -566,7 +567,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -589,7 +590,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
email: 'a@a.com',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -603,7 +604,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -616,7 +617,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -630,7 +631,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -677,7 +678,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -699,7 +700,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -720,7 +721,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
phone: '13000000001',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -734,7 +735,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -747,7 +748,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -761,7 +762,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -805,7 +806,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -827,7 +828,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -848,7 +849,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
email: 'b@a.com',
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -862,7 +863,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -875,7 +876,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
result: {
|
||||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -889,7 +890,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
|||
} from '@logto/schemas';
|
||||
import { SignUpIdentifier, logTypeGuard } from '@logto/schemas';
|
||||
import type { Nullable, Truthy } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { addSeconds, isAfter, isValid } from 'date-fns';
|
||||
import type { Context } from 'koa';
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import type { ZodType } from 'zod';
|
||||
|
@ -73,8 +73,9 @@ export const getVerificationStorageFromInteraction = async <T = VerificationStor
|
|||
};
|
||||
|
||||
export const checkValidateExpiration = (expiresAt: string) => {
|
||||
const parsed = new Date(expiresAt);
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
isValid(parsed) && isAfter(parsed, Date.now()),
|
||||
new RequestError({ code: 'session.verification_expired', status: 401 })
|
||||
);
|
||||
};
|
||||
|
@ -88,7 +89,7 @@ export const assignVerificationResult = async (
|
|||
) => {
|
||||
const verification: VerificationStorage = {
|
||||
...verificationData,
|
||||
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(Date.now(), verificationTimeout).toISOString(),
|
||||
};
|
||||
|
||||
await provider.interactionResult(ctx.req, ctx.res, {
|
||||
|
@ -116,7 +117,7 @@ export const assignContinueSignInResult = async (
|
|||
await provider.interactionResult(ctx.req, ctx.res, {
|
||||
continueSignIn: {
|
||||
...payload,
|
||||
expiresAt: dayjs().add(continueSignInTimeout, 'second').toISOString(),
|
||||
expiresAt: addSeconds(Date.now(), continueSignInTimeout).toISOString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -142,8 +143,9 @@ export const getContinueSignInResult = async (
|
|||
|
||||
const { expiresAt, ...rest } = signInResult.data.continueSignIn;
|
||||
|
||||
const parsed = new Date(expiresAt);
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
isValid(parsed) && isAfter(parsed, Date.now()),
|
||||
new RequestError({ code: 'session.unauthorized', status: 401 })
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import en from '@logto/phrases-ui/lib/locales/en';
|
||||
import fr from '@logto/phrases-ui/lib/locales/fr';
|
||||
|
||||
import { isValidStructure } from '@/utils/translation';
|
||||
import { isStrictlyPartial } from '@/utils/translation';
|
||||
|
||||
const customizedFrTranslation = {
|
||||
secondary: {
|
||||
|
@ -10,15 +10,15 @@ const customizedFrTranslation = {
|
|||
},
|
||||
};
|
||||
|
||||
describe('isValidStructure', () => {
|
||||
describe('isStrictlyPartial', () => {
|
||||
it('should be true when its structure is valid', () => {
|
||||
expect(isValidStructure(en.translation, fr.translation)).toBeTruthy();
|
||||
expect(isValidStructure(en.translation, customizedFrTranslation)).toBeTruthy();
|
||||
expect(isStrictlyPartial(en.translation, fr.translation)).toBeTruthy();
|
||||
expect(isStrictlyPartial(en.translation, customizedFrTranslation)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be true when the structure is partial and the existing key-value pairs are correct', () => {
|
||||
expect(
|
||||
isValidStructure(en.translation, {
|
||||
isStrictlyPartial(en.translation, {
|
||||
secondary: {
|
||||
sign_in_with: 'Se connecter avec {{methods, list(type: disjunction;)}}',
|
||||
// Missing 'secondary.social_bind_with' key-value pair
|
||||
|
@ -29,7 +29,7 @@ describe('isValidStructure', () => {
|
|||
|
||||
it('should be false when there is an unexpected key-value pair', () => {
|
||||
expect(
|
||||
isValidStructure(en.translation, {
|
||||
isStrictlyPartial(en.translation, {
|
||||
secondary: {
|
||||
sign_in_with: 'Se connecter avec {{methods, list(type: disjunction;)}}',
|
||||
social_bind_with:
|
||||
|
|
|
@ -1,38 +1,25 @@
|
|||
import type { Translation } from '@logto/schemas';
|
||||
|
||||
// LOG-4385: Refactor me
|
||||
// eslint-disable-next-line complexity
|
||||
export const isValidStructure = (fullTranslation: Translation, partialTranslation: Translation) => {
|
||||
const fullKeys = new Set(Object.keys(fullTranslation));
|
||||
const partialKeys = Object.keys(partialTranslation);
|
||||
|
||||
if (fullKeys.size === 0 || partialKeys.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (partialKeys.some((key) => !fullKeys.has(key))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(fullTranslation)) {
|
||||
const targetValue = partialTranslation[key];
|
||||
|
||||
if (targetValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (typeof targetValue === 'string') {
|
||||
continue;
|
||||
}
|
||||
/**
|
||||
* @param fullTranslation The translation with full keys
|
||||
* @param partialTranslation The translation to check
|
||||
* @returns If the flatten keys of `partialTranslation` is a subset of `fullTranslation`
|
||||
*/
|
||||
export const isStrictlyPartial = (
|
||||
fullTranslation: Translation,
|
||||
partialTranslation: Translation
|
||||
): boolean => {
|
||||
return Object.entries(partialTranslation).every(([key, value]) => {
|
||||
const fullValue = fullTranslation[key];
|
||||
|
||||
if (!fullValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof targetValue === 'string' || !isValidStructure(value, targetValue)) {
|
||||
return false;
|
||||
if (typeof fullValue === 'object' && typeof value === 'object') {
|
||||
return isStrictlyPartial(fullValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return typeof fullValue === typeof value;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"scripts": {},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/cli": "workspace:^"
|
||||
|
|
|
@ -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.11",
|
||||
"@logto/react": "1.0.0-beta.12",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@parcel/core": "2.7.0",
|
||||
"@parcel/transformer-sass": "2.7.0",
|
||||
|
@ -44,6 +44,17 @@
|
|||
"stylelint": "^14.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"//": "https://github.com/parcel-bundler/parcel/issues/7636",
|
||||
"targets": {
|
||||
"default": {
|
||||
"engines": {
|
||||
"browsers": "defaults"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alias": {
|
||||
"@/*": "./src/$1"
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { merge, Config } from '@silverhand/jest-config';
|
||||
import type { Config } from '@silverhand/jest-config';
|
||||
import { merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^29.1.2",
|
||||
"@logto/node": "1.0.0-beta.11",
|
||||
"@logto/js": "1.0.0-beta.11",
|
||||
"@logto/node": "1.0.0-beta.12",
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
|
@ -27,18 +28,21 @@
|
|||
"@types/node": "^16.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"eslint": "^8.21.0",
|
||||
"got": "^11.8.2",
|
||||
"got": "^11.8.5",
|
||||
"jest": "^29.1.2",
|
||||
"jest-puppeteer": "^6.1.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"openapi-schema-validator": "^12.0.0",
|
||||
"openapi-types": "^12.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"puppeteer": "^18.0.0",
|
||||
"puppeteer": "^19.0.0",
|
||||
"text-encoder": "^0.0.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import { extractCookie } from '@/utils';
|
|||
|
||||
import { MemoryStorage } from './storage';
|
||||
|
||||
const defaultConfig = {
|
||||
export const defaultConfig = {
|
||||
endpoint: logtoUrl,
|
||||
appId: demoAppApplicationId,
|
||||
persistAccessToken: false,
|
||||
|
@ -18,8 +18,8 @@ const defaultConfig = {
|
|||
|
||||
export default class MockClient {
|
||||
public interactionCookie?: string;
|
||||
private navigateUrl?: string;
|
||||
|
||||
private navigateUrl?: string;
|
||||
private readonly storage: MemoryStorage;
|
||||
private readonly logto: LogtoClient;
|
||||
|
||||
|
@ -88,6 +88,10 @@ export default class MockClient {
|
|||
return this.logto.getAccessToken(resource);
|
||||
}
|
||||
|
||||
public async getRefreshToken() {
|
||||
return this.logto.getRefreshToken();
|
||||
}
|
||||
|
||||
public async signOut(postSignOutRedirectUri?: string) {
|
||||
return this.logto.signOut(postSignOutRedirectUri);
|
||||
}
|
||||
|
|
4
packages/integration-tests/src/include.d/node-fetch.d.ts
vendored
Normal file
4
packages/integration-tests/src/include.d/node-fetch.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module 'node-fetch' {
|
||||
const nodeFetch: typeof fetch;
|
||||
export = nodeFetch;
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
import path from 'path';
|
||||
|
||||
import { fetchTokenByRefreshToken } from '@logto/js';
|
||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { signInWithUsernameAndPassword } from '@/api';
|
||||
import MockClient from '@/client';
|
||||
import MockClient, { defaultConfig } from '@/client';
|
||||
import { logtoUrl } from '@/constants';
|
||||
import { createUserByAdmin } from '@/helpers';
|
||||
import { generateUsername, generatePassword } from '@/utils';
|
||||
|
||||
|
@ -36,4 +41,41 @@ describe('get access token', () => {
|
|||
// Request for invalid resource should throw
|
||||
void expect(client.getAccessToken('api.foo.com')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => {
|
||||
const client = new MockClient({ resources: [managementResource.indicator] });
|
||||
await client.initSession();
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
assert(client.isAuthenticated, new Error('Sign in get get access token failed'));
|
||||
|
||||
const refreshToken = await client.getRefreshToken();
|
||||
assert(refreshToken, new Error('No Refresh Token found'));
|
||||
|
||||
const getAccessTokenByRefreshToken = async () =>
|
||||
fetchTokenByRefreshToken(
|
||||
{
|
||||
clientId: defaultConfig.appId,
|
||||
tokenEndpoint: path.join(logtoUrl, '/oidc/token'),
|
||||
refreshToken,
|
||||
resource: managementResource.indicator,
|
||||
},
|
||||
async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
|
||||
const response = await fetch(...args);
|
||||
assert(response.ok, new Error('Request error'));
|
||||
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
|
||||
// Allow to use the same refresh token to fetch access token within short time period
|
||||
await Promise.all([getAccessTokenByRefreshToken(), getAccessTokenByRefreshToken()]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import { languages } from '@logto/language-kit';
|
|||
import type { NormalizeKeyPaths } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import de from './locales/de';
|
||||
import en from './locales/en';
|
||||
import fr from './locales/fr';
|
||||
import ko from './locales/ko';
|
||||
|
@ -16,7 +17,7 @@ export type { LocalePhrase } from './types';
|
|||
|
||||
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
|
||||
|
||||
export const builtInLanguages = ['en', 'fr', 'pt-PT', 'zh-CN', 'ko', 'tr-TR'] as const;
|
||||
export const builtInLanguages = ['de', 'en', 'fr', 'ko', 'pt-PT', 'tr-TR', 'zh-CN'] as const;
|
||||
|
||||
export const builtInLanguageOptions = builtInLanguages.map((languageTag) => ({
|
||||
value: languageTag,
|
||||
|
@ -30,12 +31,13 @@ export type BuiltInLanguageTag = z.infer<typeof builtInLanguageTagGuard>;
|
|||
export type Resource = Record<BuiltInLanguageTag, LocalePhrase>;
|
||||
|
||||
const resource: Resource = {
|
||||
de,
|
||||
en,
|
||||
fr,
|
||||
'pt-PT': ptPT,
|
||||
'zh-CN': zhCN,
|
||||
ko,
|
||||
'pt-PT': ptPT,
|
||||
'tr-TR': trTR,
|
||||
'zh-CN': zhCN,
|
||||
};
|
||||
|
||||
export const getDefaultLanguageTag = (language: string): LanguageTag =>
|
||||
|
|
99
packages/phrases-ui/src/locales/de.ts
Normal file
99
packages/phrases-ui/src/locales/de.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import type { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
username: 'Benutzername',
|
||||
password: 'Passwort',
|
||||
email: 'Email',
|
||||
phone_number: 'Telefonnummer',
|
||||
confirm_password: 'Passwort bestätigen',
|
||||
},
|
||||
secondary: {
|
||||
sign_in_with: 'Anmelden mit {{methods, list(type: disjunction;)}}',
|
||||
register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED
|
||||
social_bind_with:
|
||||
'Besitzt du schon ein Konto? Melde dich an, um {{methods, list(type: disjunction;)}} mit deiner Identität zu verbinden.',
|
||||
},
|
||||
action: {
|
||||
sign_in: 'Anmelden',
|
||||
continue: 'Weiter',
|
||||
create_account: 'Konto erstellen',
|
||||
create: 'Erstellen',
|
||||
enter_passcode: 'Bestätigungscode eingeben',
|
||||
confirm: 'Bestätigen',
|
||||
cancel: 'Abbrechen',
|
||||
save_password: 'Speichern',
|
||||
bind: 'Mit {{address}} verknüpfen',
|
||||
back: 'Gehe zurück',
|
||||
nav_back: 'Zurück',
|
||||
agree: 'Zustimmen',
|
||||
got_it: 'Alles klar',
|
||||
sign_in_with: 'Mit {{name}} anmelden',
|
||||
forgot_password: 'Passwort vergessen?',
|
||||
switch_to: 'Zu {{method}} wechseln',
|
||||
sign_in_via_passcode: 'Sign in via verification code', // UNTRANSLATED
|
||||
sign_in_via_password: 'Sign in via password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'Email',
|
||||
phone_number: 'Telefonnummer',
|
||||
reminder: 'Erinnerung',
|
||||
not_found: '404 Nicht gefunden',
|
||||
agree_with_terms: 'Ich akzeptiere die ',
|
||||
agree_with_terms_modal: 'Bitte akzeptiere die <link></link>.',
|
||||
terms_of_use: 'Nutzungsbedingungen',
|
||||
create_account: 'Konto erstellen',
|
||||
or: 'oder',
|
||||
enter_passcode: 'Der Bestätigungscode wurde an deine {{address}} gesendet',
|
||||
passcode_sent: 'Der Bestätigungscode wurde erneut gesendet',
|
||||
resend_after_seconds: 'Nach <span>{{seconds}}</span> Sekunden erneut senden',
|
||||
resend_passcode: 'Bestätigungscode erneut senden',
|
||||
continue_with: 'Weiter mit',
|
||||
create_account_id_exists:
|
||||
'Das Konto mit {{type}} {{value}} existiert bereits, möchtest du dich anmelden?',
|
||||
sign_in_id_does_not_exists:
|
||||
'Das Konto mit {{type}} {{value}} existiert nicht, möchtest du ein neues Konto erstellen?',
|
||||
sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED
|
||||
create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', // UNTRANSLATED
|
||||
forgot_password_id_does_not_exits: 'Das Konto mit {{type}} {{value}} existiert nicht.',
|
||||
bind_account_title: 'Konto verknüpfen',
|
||||
social_create_account: 'Kein Konto? Du kannst ein neues Konto erstellen und es verknüpfen.',
|
||||
social_bind_account:
|
||||
'Besitzt du schon ein Konto? Melde dich an, um die Identität zu verknüpfen.',
|
||||
social_bind_with_existing: 'Wir haben ein Konto gefunden, das du verknüpfen kannst.',
|
||||
reset_password: 'Passwort zurücksetzen',
|
||||
reset_password_description_email:
|
||||
'Gib die Email Adresse deines Kontos ein und wir senden dir einen Bestätigungscode um dein Passwort zurückzusetzen.',
|
||||
reset_password_description_sms:
|
||||
'Gib die Telefonnummer deines Kontos ein und wir senden dir einen Bestätigungscode um dein Passwort zurückzusetzen.',
|
||||
new_password: 'Neues Passwort',
|
||||
password_changed: 'Passwort geändert',
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: 'Benutzername oder Passwort ist falsch',
|
||||
username_required: 'Benutzername ist erforderlich',
|
||||
password_required: 'Passwort ist erforderlich',
|
||||
username_exists: 'Benutzername existiert bereits',
|
||||
username_should_not_start_with_number: 'Benutzername darf nicht mit einer Zahl beginnen',
|
||||
username_valid_charset: 'Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten',
|
||||
invalid_email: 'Die Email ist ungültig',
|
||||
invalid_phone: 'Die Telefonnummer ist ungültig',
|
||||
password_min_length: 'Passwort muss mindestens {{min}} Zeichen lang sein',
|
||||
passwords_do_not_match: 'Passwörter stimmen nicht überein',
|
||||
invalid_passcode: 'Der Bestätigungscode ist ungültig',
|
||||
invalid_connector_auth: 'Die Autorisierung ist ungültig',
|
||||
invalid_connector_request: 'Connector Daten sind ungültig',
|
||||
unknown: 'Unbekannter Fehler. Versuche es später noch einmal.',
|
||||
invalid_session: 'Die Sitzung ist ungültig. Bitte melde dich erneut an.',
|
||||
},
|
||||
};
|
||||
|
||||
const de: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
export default de;
|
|
@ -25,7 +25,7 @@
|
|||
"prepack": "pnpm build"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/logto-io/logto/issues"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { languages } from '@logto/language-kit';
|
|||
import type { NormalizeKeyPaths } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import de from './locales/de';
|
||||
import en from './locales/en';
|
||||
import fr from './locales/fr';
|
||||
import ko from './locales/ko';
|
||||
|
@ -16,7 +17,7 @@ export type { LocalPhrase } from './types';
|
|||
|
||||
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
|
||||
|
||||
export const builtInLanguages = ['en', 'fr', 'pt-PT', 'zh-CN', 'ko', 'tr-TR'] as const;
|
||||
export const builtInLanguages = ['de', 'en', 'fr', 'ko', 'pt-PT', 'tr-TR', 'zh-CN'] as const;
|
||||
|
||||
export const builtInLanguageOptions = builtInLanguages.map((languageTag) => ({
|
||||
value: languageTag,
|
||||
|
@ -42,12 +43,13 @@ export const isBuiltInLanguageTag = (language: string): language is BuiltInLangu
|
|||
export type Resource = Record<BuiltInLanguageTag, LocalPhrase>;
|
||||
|
||||
const resource: Resource = {
|
||||
de,
|
||||
en,
|
||||
fr,
|
||||
'pt-PT': ptPT,
|
||||
'zh-CN': zhCN,
|
||||
ko,
|
||||
'pt-PT': ptPT,
|
||||
'tr-TR': trTR,
|
||||
'zh-CN': zhCN,
|
||||
};
|
||||
|
||||
export default resource;
|
||||
|
|
152
packages/phrases/src/locales/de/errors.ts
Normal file
152
packages/phrases/src/locales/de/errors.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
const errors = {
|
||||
auth: {
|
||||
authorization_header_missing: 'Autorisierungs-Header fehlt.',
|
||||
authorization_token_type_not_supported: 'Autorisierungs-Typ wird nicht unterstützt.',
|
||||
unauthorized: 'Unautorisiert. Bitte überprüfe deine Zugangsdaten.',
|
||||
forbidden: 'Verboten. Bitte überprüfe deine Rollen und Berechtigungen.',
|
||||
expected_role_not_found:
|
||||
'Erwartete Rolle nicht gefunden. Bitte überprüfe deine Rollen und Berechtigungen.',
|
||||
jwt_sub_missing: '`sub` fehlt in JWT.',
|
||||
},
|
||||
guard: {
|
||||
invalid_input: 'Die Anfrage {{type}} ist ungültig.',
|
||||
invalid_pagination: 'Die Paginierung der Anfrage ist ungültig.',
|
||||
},
|
||||
oidc: {
|
||||
aborted: 'Der Endnutzer hat die Interaktion abgebrochen.',
|
||||
invalid_scope: 'Scope {{scope}} wird nicht unterstützt.',
|
||||
invalid_scope_plural: 'Scopes {{scopes}} werden nicht unterstützt.',
|
||||
invalid_token: 'Ungültiger Token übermittelt.',
|
||||
invalid_client_metadata: 'Ungültige Client Metadaten übermittelt.',
|
||||
insufficient_scope: 'Access token fehlen angefragte scope {{scopes}}.',
|
||||
invalid_request: 'Anfrage ist ungültig.',
|
||||
invalid_grant: 'Grant request ist ungültig.',
|
||||
invalid_redirect_uri:
|
||||
'`redirect_uri` stimmt nicht mit den registrierten `redirect_uris` des Clients überein.',
|
||||
access_denied: 'Zugang verweigert.',
|
||||
invalid_target: 'Ungültiger resource indicator.',
|
||||
unsupported_grant_type: 'Nicht unterstützter `grant_type` angefragt.',
|
||||
unsupported_response_mode: 'Nicht unterstützter `response_mode` angefragt.',
|
||||
unsupported_response_type: 'Nicht unterstützter `response_type` angefragt.',
|
||||
provider_error: 'OIDC interner Fehler: {{message}}.',
|
||||
},
|
||||
user: {
|
||||
username_exists_register: 'Der Benutzername wurde registriert.',
|
||||
email_exists_register: 'Die E-Mail wurde registriert.',
|
||||
phone_exists_register: 'Die Telefonnummer wurde registriert.',
|
||||
invalid_email: 'Ungültige E-Mail.',
|
||||
invalid_phone: 'Ungültige Telefonnummer.',
|
||||
email_not_exists: 'Die E-Mail wurde noch nicht registriert.',
|
||||
phone_not_exists: 'Die Telefonnummer wurde noch nicht registriert.',
|
||||
identity_not_exists: 'Die Identität wurde noch nicht registriert.',
|
||||
identity_exists: 'Die Identität wurde registriert.',
|
||||
invalid_role_names: 'Rollennamen ({{roleNames}}) sind ungültig',
|
||||
cannot_delete_self: 'Du kannst dich nicht selbst löschen.',
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
||||
same_password: 'Das neue Passwort muss sich vom alten unterscheiden.',
|
||||
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
|
||||
password_exists: 'Your password has been set.', // UNTRANSLATED
|
||||
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
|
||||
username_exists: 'Your username has been set.', // UNTRANSLATED
|
||||
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
|
||||
email_exists: 'Your email has been set.', // UNTRANSLATED
|
||||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',
|
||||
pepper_not_found: 'Password pepper not found. Please check your core envs.',
|
||||
},
|
||||
session: {
|
||||
not_found: 'Sitzung nicht gefunden. Bitte melde dich erneut an.',
|
||||
invalid_credentials: 'Ungültige Zugangsdaten. Überprüfe deine Eingaben.',
|
||||
invalid_sign_in_method: 'Aktuelle Anmeldemethode ist ungültig.',
|
||||
invalid_connector_id: 'Connector mit ID {{connectorId}} wurde nicht gefunden.',
|
||||
insufficient_info: 'Unzureichende Informationen für die Anmeldung.',
|
||||
connector_id_mismatch: 'Connector ID stimmt nicht mit Sitzung überein.',
|
||||
connector_session_not_found: 'Connector Sitzung nicht gefunden. Bitte melde dich erneut an.',
|
||||
verification_session_not_found:
|
||||
'Die Verifizierung war nicht erfolgreich. Starte die Verifizierung neu und versuche es erneut.',
|
||||
verification_expired:
|
||||
'Die Verbindung wurde unterbrochen. Verifiziere erneut, um die Sicherheit deines Kontos zu gewährleisten.',
|
||||
unauthorized: 'Bitte melde dich erst an.',
|
||||
unsupported_prompt_name: 'Nicht unterstützter prompt Name.',
|
||||
forgot_password_not_enabled: 'Forgot password is not enabled.',
|
||||
},
|
||||
connector: {
|
||||
// UNTRANSLATED
|
||||
general: 'An unexpected error occurred in connector.{{errorDescription}}',
|
||||
not_found: 'Cannot find any available connector for type: {{type}}.',
|
||||
not_enabled: 'The connector is not enabled.',
|
||||
invalid_metadata: "The connector's metadata is invalid.",
|
||||
invalid_config_guard: "The connector's config guard is invalid.",
|
||||
unexpected_type: "The connector's type is unexpected.",
|
||||
invalid_request_parameters: 'The request is with wrong input parameter(s).',
|
||||
insufficient_request_parameters: 'The request might miss some input parameters.',
|
||||
invalid_config: "The connector's config is invalid.",
|
||||
invalid_response: "The connector's response is invalid.",
|
||||
template_not_found: 'Unable to find correct template in connector config.',
|
||||
not_implemented: '{{method}}: has not been implemented yet.',
|
||||
social_invalid_access_token: "The connector's access token is invalid.",
|
||||
invalid_auth_code: "The connector's auth code is invalid.",
|
||||
social_invalid_id_token: "The connector's id token is invalid.",
|
||||
authorization_failed: "The user's authorization process is unsuccessful.",
|
||||
social_auth_code_invalid: 'Unable to get access token, please check authorization code.',
|
||||
more_than_one_sms: 'The number of SMS connectors is larger then 1.',
|
||||
more_than_one_email: 'The number of Email connectors is larger then 1.',
|
||||
db_connector_type_mismatch: 'There is a connector in the DB that does not match the type.',
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.',
|
||||
not_found: 'Passcode nicht gefunden. Bitte sende erst einen Passcode.',
|
||||
phone_mismatch:
|
||||
'Telefonnummer stimmt nicht mit Passcode überein. Frage einen neuen Passcode an.',
|
||||
email_mismatch: 'E-Mail stimmt nicht mit Passcode überein. Frage einen neuen Passcode an.',
|
||||
code_mismatch: 'Ungültiger Passcode.',
|
||||
expired: 'Passcode ist abgelaufen. Frage einen neuen Passcode an.',
|
||||
exceed_max_try: 'Passcode wurde zu oft versucht. Frage einen neuen Passcode an.',
|
||||
},
|
||||
sign_in_experiences: {
|
||||
empty_content_url_of_terms_of_use:
|
||||
'Leere "Nutzungsbedingungen" URL. Bitte füge die URL hinzu, wenn "Nutzungsbedingungen" aktiviert ist.',
|
||||
empty_logo: 'Bitte füge eine Logo URL hinzu.',
|
||||
empty_slogan:
|
||||
'Leerer Branding-Slogan. Bitte füge einen Branding-Slogan hinzu, wenn ein UI-Stil ausgewählt wird, der den Slogan enthält.',
|
||||
empty_social_connectors:
|
||||
'Leere Social Connectors. Bitte füge aktivierte Social Connectoren hinzu, wenn Social Anmeldung aktiviert ist.',
|
||||
enabled_connector_not_found: 'Aktivierter {{type}} Connector nicht gefunden.',
|
||||
not_one_and_only_one_primary_sign_in_method:
|
||||
'Es darf nur eine primäre Anmeldemethode geben. Bitte überprüfe deine Eingabe.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
unsupported_default_language: 'Die Sprache - {{language}} wird momentan nicht unterstützt.',
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
'{{languageTag}} ist die Standard-Sprache und kann nicht gelöscht werden.',
|
||||
invalid_translation_structure: 'Ungültige Übersetzungsstruktur. Bitte überprüfe deine Eingabe.',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: 'Ungültiger Zod Typ. Überprüfe deine route guard Konfiguration.',
|
||||
not_supported_zod_type_for_params:
|
||||
'Nicht unterstützter Zod Typ für diese Parameter. Überprüfe deine route guard Konfiguration.',
|
||||
},
|
||||
entity: {
|
||||
create_failed: 'Fehler beim erstellen von {{name}}.',
|
||||
not_exists: '{{name}} existiert nicht.',
|
||||
not_exists_with_id: '{{name}} mit ID `{{id}}` existiert nicht.',
|
||||
not_found: 'Die Ressource wurde nicht gefunden.',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'Der Log Typ ist ungültig.',
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
10
packages/phrases/src/locales/de/index.ts
Normal file
10
packages/phrases/src/locales/de/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type { LocalPhrase } from '../../types';
|
||||
import errors from './errors';
|
||||
import translation from './translation';
|
||||
|
||||
const de: LocalPhrase = Object.freeze({
|
||||
translation,
|
||||
errors,
|
||||
});
|
||||
|
||||
export default de;
|
|
@ -0,0 +1,11 @@
|
|||
const api_resource_details = {
|
||||
back_to_api_resources: 'Zurück zu API Ressourcen',
|
||||
token_expiration_time_in_seconds: 'Token Ablaufzeit (in Sekunden)',
|
||||
token_expiration_time_in_seconds_placeholder: 'Gib die Ablaufzeit des Tokens ein',
|
||||
delete_description:
|
||||
'Diese Aktion kann nicht rückgängig gemacht werden. Die API Ressource wird permanent gelöscht. Bitte gib den API Ressourcennamen <span>{{name}}</span> zur Bestätigung ein.',
|
||||
enter_your_api_resource_name: 'Gib einen API Ressourcennamen ein',
|
||||
api_resource_deleted: 'Die API Ressource {{name}} wurde erfolgreich gelöscht',
|
||||
};
|
||||
|
||||
export default api_resource_details;
|
|
@ -0,0 +1,14 @@
|
|||
const api_resources = {
|
||||
title: 'API Ressourcen',
|
||||
subtitle: 'Lege APIs an, die du in deinen autorisierten Anwendungen verwenden kannst',
|
||||
create: 'Erstelle API Ressource',
|
||||
api_name: 'API Name',
|
||||
api_name_placeholder: 'Gib einen API Namen ein',
|
||||
api_identifier: 'API Identifikator',
|
||||
api_identifier_tip:
|
||||
'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem Ressourcen Parameter in OAuth 2.0.',
|
||||
api_resource_created: 'Die API Ressource {{name}} wurde erfolgreich angelegt',
|
||||
api_identifier_placeholder: 'https://dein-api-identifikator/',
|
||||
};
|
||||
|
||||
export default api_resources;
|
|
@ -0,0 +1,44 @@
|
|||
const application_details = {
|
||||
back_to_applications: 'Zurück zu Anwendungen',
|
||||
check_guide: 'Zur Anleitung',
|
||||
advanced_settings: 'Erweiterte Einstellungen',
|
||||
application_name: 'Anwendungsname',
|
||||
application_name_placeholder: 'Meine App',
|
||||
description: 'Beschreibung',
|
||||
description_placeholder: 'Gib eine Beschreibung ein',
|
||||
authorization_endpoint: 'Autorisierungs-Endpoint',
|
||||
authorization_endpoint_tip:
|
||||
'Der Endpoint, der für die Authentifizierung und Autorisierung via OpenID Connect verwendet wird.',
|
||||
application_id: 'App ID',
|
||||
application_secret: 'App Geheimnis',
|
||||
redirect_uri: 'Umleitungs-URI',
|
||||
redirect_uris: 'Umleitungs-URIs',
|
||||
redirect_uri_placeholder: 'https://deine.website.de/app',
|
||||
redirect_uri_placeholder_native: 'io.logto://callback',
|
||||
redirect_uri_tip:
|
||||
'URI zu der der Benutzer nach der Anmeldung (egal ob erfolgreich oder nicht) weitergeleitet wird. See OpenID Connect AuthRequest for more info.',
|
||||
post_sign_out_redirect_uri: 'Post Sign-out Umleitungs-URI',
|
||||
post_sign_out_redirect_uris: 'Post Sign-out Umleitungs-URIs',
|
||||
post_sign_out_redirect_uri_placeholder: 'https://deine.website.de/home',
|
||||
post_sign_out_redirect_uri_tip:
|
||||
'URI zu der der Benutzer nach dem Abmelden weitergeleitet wird (optional). Hat bei einigen Anwendungstypen keine Auswirkungen.',
|
||||
cors_allowed_origins: 'CORS allowed origins',
|
||||
cors_allowed_origins_placeholder: 'https://your.website.de',
|
||||
cors_allowed_origins_tip:
|
||||
'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich.',
|
||||
add_another: 'Weitere hinzufügen',
|
||||
id_token_expiration: 'ID Token Ablaufzeit',
|
||||
refresh_token_expiration: 'Refresh Token Ablaufzeit',
|
||||
token_endpoint: 'Token Endpoint',
|
||||
user_info_endpoint: 'Benutzerinformations-Endpoint',
|
||||
enable_admin_access: 'Admin-Zugang aktivieren',
|
||||
enable_admin_access_label:
|
||||
'Zugang zur Management API aktivieren oder deaktivieren. Falls aktiviert, können access tokens verwendet werden, um die Management API im Namen der Anwendung aufzurufen.',
|
||||
delete_description:
|
||||
'Diese Aktion kann nicht rückgängig gemacht werden. Die Anwendung wird permanent gelöscht. Bitte gib den Anwendungsnamen <span>{{name}}</span> zur Bestätigung ein.',
|
||||
enter_your_application_name: 'Gib einen Anwendungsnamen ein',
|
||||
application_deleted: 'Anwendung {{name}} wurde erfolgreich gelöscht',
|
||||
redirect_uri_required: 'Gebe mindestens eine Umleitungs-URI an',
|
||||
};
|
||||
|
||||
export default application_details;
|
|
@ -0,0 +1,50 @@
|
|||
const applications = {
|
||||
title: 'Anwendungen',
|
||||
subtitle:
|
||||
'Richte eine native, Single Page oder herkömmliche Anwendung ein, die Logto zur Authentifizierung nutzt.',
|
||||
create: 'Anwendung erstellen',
|
||||
application_name: 'Anwendungsname',
|
||||
application_name_placeholder: 'Meine App',
|
||||
application_description: 'Anwendungsbeschreibung',
|
||||
application_description_placeholder: 'Gib eine Beschreibung ein',
|
||||
select_application_type: 'Wähle einen Anwendungstyp',
|
||||
no_application_type_selected: 'Du hast noch keinen Anwendungstyp ausgewählt',
|
||||
application_created:
|
||||
'Die Anwendung {{name}} wurde erfolgreich erstellt! \nKonfiguriere jetzt die Anwendung.',
|
||||
app_id: 'App ID',
|
||||
type: {
|
||||
native: {
|
||||
title: 'Native App',
|
||||
subtitle: 'Eine Anwendung, die in einer nativen Umgebung läuft',
|
||||
description: 'z.B. iOS app, Android app',
|
||||
},
|
||||
spa: {
|
||||
title: 'Single Page App',
|
||||
subtitle:
|
||||
'Eine Anwendung, die in einem Webbrowser ausgeführt wird und Daten dynamisch an Ort und Stelle aktualisiert',
|
||||
description: 'z.B. React DOM app, Vue app',
|
||||
},
|
||||
traditional: {
|
||||
title: 'Herkömmliche Website',
|
||||
subtitle: 'Eine Anwendung, die Seiten allein durch den Webserver rendert und aktualisiert',
|
||||
description: 'z.B. Next.js, PHP',
|
||||
},
|
||||
machine_to_machine: {
|
||||
title: 'Machine to Machine',
|
||||
subtitle: 'Eine Anwendung (normalerweise ein Dienst), die direkt mit Ressourcen kommuniziert',
|
||||
description: 'z.B. Backend Dienst',
|
||||
},
|
||||
},
|
||||
guide: {
|
||||
get_sample_file: 'Zum Beispielprojekt',
|
||||
header_description:
|
||||
'Folge der Schritt-für-Schritt-Anleitung, um die Anwendung zu integrieren, oder klick auf die rechte Schaltfläche, um unser Beispielprojekt zu erhalten',
|
||||
title: 'Die Anwendung wurde erfolgreich erstellt',
|
||||
subtitle:
|
||||
'Folge nun den folgenden Schritten, um deine App-Einstellungen abzuschließen. Bitte wähle den SDK-Typ aus, um fortzufahren.',
|
||||
description_by_sdk:
|
||||
'Diese Schnellstart-Anleitung zeigt, wie man Logto in die {{sdk}} App integriert',
|
||||
},
|
||||
};
|
||||
|
||||
export default applications;
|
|
@ -0,0 +1,22 @@
|
|||
const connector_details = {
|
||||
back_to_connectors: 'Zurück zu Connectoren',
|
||||
check_readme: 'Zur README',
|
||||
save_error_empty_config: 'Bitte fülle die Konfiguration aus',
|
||||
send: 'Senden',
|
||||
send_error_invalid_format: 'Ungültige Eingabe',
|
||||
edit_config_label: 'Gib deine JSON-Konfiguration ein',
|
||||
test_email_sender: 'Teste den E-Mail Connector',
|
||||
test_sms_sender: 'Teste den SMS Connector',
|
||||
test_email_placeholder: 'Gib eine Test-E-Mail ein',
|
||||
test_sms_placeholder: 'Gib eine Test-Telefonnummer ein',
|
||||
test_message_sent: 'Testnachricht wurde gesendet!',
|
||||
test_sender_description: 'Wenn dein JSON richtig konfiguriert ist, erhältst du eine Nachricht.',
|
||||
options_change_email: 'E-Mail Connector bearbeiten',
|
||||
options_change_sms: 'SMS Connector bearbeiten',
|
||||
connector_deleted: 'Der Connector wurde erfolgreich gelöscht',
|
||||
type_email: 'E-Mail connector',
|
||||
type_sms: 'SMS connector',
|
||||
type_social: 'Social connector',
|
||||
};
|
||||
|
||||
export default connector_details;
|
|
@ -0,0 +1,39 @@
|
|||
const connectors = {
|
||||
title: 'Connectoren',
|
||||
subtitle: 'Richte Connectoren ein um passwortlose und Social Anmeldung zu aktivieren',
|
||||
create: 'Social Connector erstellen',
|
||||
config_sie_notice: 'You’ve set up connectors. Make sure to configure it in <a>{{link}}</a>.', // UNTRANSLATED
|
||||
config_sie_link_text: 'sign in experience', // UNTRANSLATED
|
||||
tab_email_sms: 'E-Mail und SMS Connectoren',
|
||||
tab_social: 'Social Connectoren',
|
||||
connector_name: 'Connectorname',
|
||||
connector_type: 'Typ',
|
||||
connector_status: 'Anmeldeoberfläche',
|
||||
connector_status_in_use: 'In Benutzung',
|
||||
connector_status_not_in_use: 'Nicht in Benutzung',
|
||||
social_connector_eg: 'z.B. Google, Facebook, Github',
|
||||
save_and_done: 'Speichern und fertigstellen',
|
||||
type: {
|
||||
email: 'E-Mail Connector',
|
||||
sms: 'SMS Connector',
|
||||
social: 'Social Connector',
|
||||
},
|
||||
setup_title: {
|
||||
email: 'E-Mail Connector einrichten',
|
||||
sms: 'SMS Connector einrichten',
|
||||
social: 'Social Connector erstellen',
|
||||
},
|
||||
guide: {
|
||||
subtitle: 'Eine Schritt-für-Schritt-Anleitung zur Konfiguration deines Connectors',
|
||||
},
|
||||
platform: {
|
||||
universal: 'Universal',
|
||||
web: 'Web',
|
||||
native: 'Nativ',
|
||||
},
|
||||
add_multi_platform: ' unterstützt mehrere Plattformen, wähle eine Plattform aus, um fortzufahren',
|
||||
drawer_title: 'Connector Anleitung',
|
||||
drawer_subtitle: 'Folge den Anweisungen, um deinen Connector zu integrieren',
|
||||
};
|
||||
|
||||
export default connectors;
|
|
@ -0,0 +1,22 @@
|
|||
const contact = {
|
||||
title: 'Kontakt',
|
||||
description:
|
||||
'Tritt unserer Community bei, um Feedback zu geben, um Hilfe zu bitten und deine Gedanken mit anderen Entwicklern zu teilen',
|
||||
discord: {
|
||||
title: 'Discord channel',
|
||||
description: 'Tritt unserem öffentlichen Kanal bei, um mit anderen Entwicklern zu chatten',
|
||||
button: 'Beitreten',
|
||||
},
|
||||
github: {
|
||||
title: 'GitHub',
|
||||
description: 'Erstelle ein Issue bei GitHub',
|
||||
button: 'Öffnen',
|
||||
},
|
||||
email: {
|
||||
title: 'E-Mail senden',
|
||||
description: 'Schick uns eine E-Mail für weitere Informationen und Hilfe',
|
||||
button: 'Senden',
|
||||
},
|
||||
};
|
||||
|
||||
export default contact;
|
|
@ -0,0 +1,22 @@
|
|||
const dashboard = {
|
||||
title: 'Dashboard',
|
||||
description: 'Verschaffe dir einen Überblick über die Leistung deiner App',
|
||||
total_users: 'Gesamtzahl der Benutzer',
|
||||
total_users_tip: 'Gesamtzahl der Benutzer',
|
||||
new_users_today: 'Neue Benutzer heute',
|
||||
new_users_today_tip: 'Neue Benutzer, die sich heute in deinen Anwendungen registriert haben',
|
||||
new_users_7_days: 'Neue Benutzer in den letzten 7 Tagen',
|
||||
new_users_7_days_tip:
|
||||
'Neue Benutzer, die sich in den letzten 7 Tagen in deinen Anwendungen registriert haben',
|
||||
daily_active_users: 'Täglich aktive Benutzer',
|
||||
daily_active_users_tip:
|
||||
'Die Anzahl der einzelnen Benutzer, die heute Token in deinen Anwendungen ausgetauscht haben',
|
||||
weekly_active_users: 'Wöchentlich aktive Benutzer',
|
||||
weekly_active_users_tip:
|
||||
'Die Anzahl der einzelnen Benutzer, die in den letzten 7 Tagen Token in deinen Anwendungen ausgetauscht haben',
|
||||
monthly_active_users: 'Monatlich aktive Benutzer',
|
||||
monthly_active_users_tip:
|
||||
'Die Anzahl der einzelnen Benutzer, die in den letzten 30 Tagen Token in deinen Anwendungen ausgetauscht haben',
|
||||
};
|
||||
|
||||
export default dashboard;
|
|
@ -0,0 +1,21 @@
|
|||
const errors = {
|
||||
something_went_wrong: 'Ups, da ist etwas schief gelaufen.',
|
||||
page_not_found: 'Seite nicht gefunden',
|
||||
unknown_server_error: 'Unbekannter Serverfehler',
|
||||
empty: 'Keine Daten verfügbar',
|
||||
missing_total_number: 'Total-Number wurde nicht in Response Headern gefunden',
|
||||
invalid_uri_format: 'Ungültiges URI-Format',
|
||||
invalid_origin_format: 'Ungültiges URI Origin-Format',
|
||||
invalid_json_format: 'Ungültiges JSON-Format',
|
||||
invalid_error_message_format: 'Ungültiges Fehlermeldung-Format.',
|
||||
required_field_missing: 'Bitte fülle {{field}} aus',
|
||||
required_field_missing_plural: 'Mindestens ein {{field}} muss ausgefüllt sein',
|
||||
more_details: 'Mehr Details',
|
||||
username_pattern_error:
|
||||
'Der Benutzername sollte nur Buchstaben, Zahlen oder Unterstriche enthalten und nicht mit einer Zahl beginnen.',
|
||||
password_pattern_error: 'Das Passwort muss aus mindestens 6 Zeichen lang sein',
|
||||
insecure_contexts: 'Unsichere Kontexte (nicht-HTTPS) werden nicht unterstützt.',
|
||||
unexpected_error: 'Ein unerwarteter Fehler ist aufgetreten',
|
||||
};
|
||||
|
||||
export default errors;
|
|
@ -0,0 +1,43 @@
|
|||
const general = {
|
||||
placeholder: 'Platzhalter',
|
||||
skip: 'Überspringen',
|
||||
next: 'Weiter',
|
||||
retry: 'Erneut versuchen',
|
||||
done: 'Fertig',
|
||||
search: 'Suche',
|
||||
search_placeholder: 'Suchen',
|
||||
clear_result: 'Ergebnisse löschen',
|
||||
save: 'Speichern',
|
||||
save_changes: 'Änderungen speichern',
|
||||
saved: 'Gespeichert!',
|
||||
loading: 'Lade...',
|
||||
redirecting: 'Weiterleiten...',
|
||||
add: 'Hinzufügen',
|
||||
added: 'Hinzugefügt',
|
||||
cancel: 'Abbrechen',
|
||||
confirm: 'Bestätigen',
|
||||
check_out: 'Ansehen',
|
||||
create: 'Erstellen',
|
||||
set_up: 'Einrichten',
|
||||
customize: 'Anpassen',
|
||||
enable: 'Aktivieren',
|
||||
reminder: 'Erinnerung',
|
||||
delete: 'Löschen',
|
||||
more_options: 'MEHR OPTIONEN',
|
||||
close: 'Schließen',
|
||||
copy: 'Kopieren',
|
||||
copying: 'Kopiere',
|
||||
copied: 'Kopiert',
|
||||
required: 'Erforderlich',
|
||||
add_another: '+ Weitere hinzufügen',
|
||||
deletion_confirmation: 'Willst du {{title}} wirklich löschen?',
|
||||
settings_nav: 'Einstellungen',
|
||||
unsaved_changes_warning:
|
||||
'Du hast ungespeicherte Änderungen. Willst du diese Seite wirklich verlassen?',
|
||||
leave_page: 'Seite verlassen',
|
||||
stay_on_page: 'Auf Seite bleiben',
|
||||
type_to_search: 'Tippe um zu suchen',
|
||||
got_it: 'Got it', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
|
@ -0,0 +1,29 @@
|
|||
const get_started = {
|
||||
progress: 'Erste Schritte: {{completed}}/{{total}}',
|
||||
progress_dropdown_title: 'Was du machen kannst...',
|
||||
title: 'Wie willst du mit Logto loslegen?',
|
||||
subtitle_part1: 'Ein paar Dinge, die du tun kannst, um schnell von Logto zu profitieren',
|
||||
subtitle_part2: 'Ich bin fertig mit der Einrichtung.',
|
||||
hide_this: 'Ausblenden',
|
||||
confirm_message:
|
||||
'Bist du sicher, dass du diese Seite ausblenden willst? Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||
card1_title: 'Zur Demo',
|
||||
card1_subtitle: 'Probiere die Logto-Anmeldung jetzt aus, um zu sehen, wie sie funktioniert',
|
||||
card2_title: 'Erste Anwendung erstellen und integrieren',
|
||||
card2_subtitle:
|
||||
'Richte eine native, Single Page oder herkömmliche Anwendung ein, die Logto zur Authentifizierung nutzt.',
|
||||
card3_title: 'Anmeldeoberfläche anpassen',
|
||||
card3_subtitle:
|
||||
'Passe die Benutzeroberfläche für die Anmeldung an deine Marke an und zeige eine Vorschau in Echtzeit an',
|
||||
card4_title: 'SMS- und E-Mail-Verbindung einrichten',
|
||||
card4_subtitle:
|
||||
'Probiere die passwortlose Anmeldung mit Telefonnummer oder E-Mail aus, um ein sicheres und reibungsloses Kundenerlebnis zu ermöglichen.',
|
||||
card5_title: 'Social Connector hinzufügen',
|
||||
card5_subtitle:
|
||||
'Lass deine Kunden sich mit einem Klick mit ihren sozialen Identitäten bei deiner App anmelden',
|
||||
card6_title: 'Weitere Informationen',
|
||||
card6_subtitle:
|
||||
'Schau dir unsere schrittweisen, szenariobasierten Dokumentationen ohne langweilige Konzepte an',
|
||||
};
|
||||
|
||||
export default get_started;
|
|
@ -0,0 +1,52 @@
|
|||
import api_resource_details from './api-resource-details';
|
||||
import api_resources from './api-resources';
|
||||
import application_details from './application-details';
|
||||
import applications from './applications';
|
||||
import connector_details from './connector-details';
|
||||
import connectors from './connectors';
|
||||
import contact from './contact';
|
||||
import dashboard from './dashboard';
|
||||
import errors from './errors';
|
||||
import general from './general';
|
||||
import get_started from './get-started';
|
||||
import log_details from './log-details';
|
||||
import logs from './logs';
|
||||
import session_expired from './session-expired';
|
||||
import settings from './settings';
|
||||
import sign_in_exp from './sign-in-exp';
|
||||
import tab_sections from './tab-sections';
|
||||
import tabs from './tabs';
|
||||
import user_details from './user-details';
|
||||
import users from './users';
|
||||
import welcome from './welcome';
|
||||
|
||||
const admin_console = {
|
||||
title: 'Admin Konsole',
|
||||
sign_out: 'Abmelden',
|
||||
profile: 'Profil',
|
||||
admin_user: 'Admin',
|
||||
system_app: 'System',
|
||||
general,
|
||||
errors,
|
||||
tab_sections,
|
||||
tabs,
|
||||
applications,
|
||||
application_details,
|
||||
api_resources,
|
||||
api_resource_details,
|
||||
connectors,
|
||||
connector_details,
|
||||
get_started,
|
||||
users,
|
||||
user_details,
|
||||
contact,
|
||||
sign_in_exp,
|
||||
settings,
|
||||
dashboard,
|
||||
logs,
|
||||
log_details,
|
||||
session_expired,
|
||||
welcome,
|
||||
};
|
||||
|
||||
export default admin_console;
|
|
@ -0,0 +1,17 @@
|
|||
const log_details = {
|
||||
back_to_logs: 'Zurück zu Audit Logs',
|
||||
back_to_user: 'Zurück zu {{name}}',
|
||||
success: 'Erfolgreich',
|
||||
failed: 'Fehlgeschlagen',
|
||||
event_type: 'Event Typ',
|
||||
application: 'Anwendung',
|
||||
ip_address: 'IP Adresse',
|
||||
user: 'Benutzer',
|
||||
log_id: 'Log ID',
|
||||
time: 'Zeit',
|
||||
user_agent: 'User agent',
|
||||
tab_details: 'Details',
|
||||
raw_data: 'Rohe Daten',
|
||||
};
|
||||
|
||||
export default log_details;
|
|
@ -0,0 +1,12 @@
|
|||
const logs = {
|
||||
title: 'Audit Logs',
|
||||
subtitle:
|
||||
'Anzeige der Log Daten von Authentifizierungsereignissen, die von Admins und Benutzern stammen',
|
||||
event: 'Event',
|
||||
user: 'Benutzer',
|
||||
application: 'Anwendung',
|
||||
time: 'Zeit',
|
||||
filter_by: 'Filter nach',
|
||||
};
|
||||
|
||||
export default logs;
|
|
@ -0,0 +1,8 @@
|
|||
const session_expired = {
|
||||
title: 'Sitzung abgelaufen',
|
||||
subtitle:
|
||||
'Deine Sitzung ist möglicherweise abgelaufen und deine Verbindung wurde unterbrochen. Klicke auf die Schaltfläche unten, um dich erneut an der Admin Konsole anzumelden.',
|
||||
button: 'Erneut anmelden',
|
||||
};
|
||||
|
||||
export default session_expired;
|
|
@ -0,0 +1,27 @@
|
|||
const settings = {
|
||||
title: 'Einstellungen',
|
||||
description: 'Verwalte die globalen Einstellungen',
|
||||
tabs: {
|
||||
general: 'Allgemein',
|
||||
},
|
||||
custom_domain: 'Benutzerdefinierte Domain',
|
||||
language: 'Sprache',
|
||||
appearance: 'Darstellung',
|
||||
appearance_system: 'Synchonisiere mit Systemeinstellungen',
|
||||
appearance_light: 'Hell',
|
||||
appearance_dark: 'Dunkel',
|
||||
saved: 'Gespeichert!',
|
||||
change_password: 'Passwort ändern',
|
||||
change_password_description:
|
||||
'Du kannst das Passwort für dieses Konto ändern. Du verwendest den aktuellen Benutzernamen mit dem neuen Passwort, um dich in der Admin Konsole anzumelden.',
|
||||
change_modal_title: 'Account Password ändern',
|
||||
change_modal_description:
|
||||
'Du verwendest den aktuellen Benutzernamen mit dem neuen Passwort, um dich in der Admin Konsole anzumelden.',
|
||||
new_password: 'Neues Passwort',
|
||||
new_password_placeholder: 'Gib ein neues Passwort ein',
|
||||
confirm_password: 'Passwort bestätigen',
|
||||
confirm_password_placeholder: 'Bestätige das neue Passwort',
|
||||
password_changed: 'Passwort geändert!',
|
||||
};
|
||||
|
||||
export default settings;
|
|
@ -0,0 +1,187 @@
|
|||
const sign_in_exp = {
|
||||
title: 'Anmeldeoberfläche',
|
||||
description:
|
||||
'Passe die Benutzeroberfläche für die Anmeldung an deine Marke an und zeige eine Vorschau in Echtzeit an',
|
||||
tabs: {
|
||||
branding: 'Branding',
|
||||
methods: 'Anmeldemethoden',
|
||||
sign_up_and_sign_in: 'Sign up and Sign in', // UNTRANSLATED
|
||||
others: 'Andere',
|
||||
},
|
||||
welcome: {
|
||||
title:
|
||||
'Dies ist das erste Mal, dass du deine Anmeldeoberfläche anpasst. Diese Anleitung hilft dir, alle notwendigen Einstellungen vorzunehmen und schnell loszulegen.',
|
||||
get_started: 'Erste Schritte',
|
||||
apply_remind:
|
||||
'Bitte beachte, dass die Anmeldeoberfläche für alle Anwendungen unter diesem Konto gilt.',
|
||||
got_it: 'Alles klar',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
},
|
||||
color: {
|
||||
title: 'FARBE',
|
||||
primary_color: 'Markenfarbe',
|
||||
dark_primary_color: 'Markenfarbe (Dunkler Modus)',
|
||||
dark_mode: 'Aktiviere Dunklen Modus',
|
||||
dark_mode_description:
|
||||
'Deine App erhält einen automatisch generierten Dunklen Modus, der auf deiner Markenfarbe und dem Logto-Algorithmus basiert. Du kannst diesen nach Belieben anpassen.',
|
||||
dark_mode_reset_tip: 'Neuberechnung der Farbe des dunklen Modus basierend auf der Markenfarbe.',
|
||||
reset: 'Neuberechnen',
|
||||
},
|
||||
branding: {
|
||||
title: 'BRANDING',
|
||||
ui_style: 'Stil',
|
||||
styles: {
|
||||
logo_slogan: 'App logo mit Slogan',
|
||||
logo: 'Nur App logo',
|
||||
},
|
||||
logo_image_url: 'App logo URL',
|
||||
logo_image_url_placeholder: 'https://dein.cdn.domain/logo.png',
|
||||
dark_logo_image_url: 'App logo URL (Dunkler Modus)',
|
||||
dark_logo_image_url_placeholder: 'https://dein.cdn.domain/logo-dark.png',
|
||||
slogan: 'Slogan',
|
||||
slogan_placeholder: 'Entfessle deine Kreativität',
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'ANMELDEMETHODEN',
|
||||
primary: 'Primäre Anmeldemethode',
|
||||
enable_secondary: 'Aktiviere sekundäre Anmeldemethoden',
|
||||
enable_secondary_description:
|
||||
'Sobald sie aktiviert ist, unterstützt deine App neben der primären Anmeldemethode noch weitere Anmeldemethoden. ',
|
||||
methods: 'Anmeldemethode',
|
||||
methods_sms: 'SMS Anmeldung',
|
||||
methods_email: 'E-Mail Anmeldung',
|
||||
methods_social: 'Social Anmeldung',
|
||||
methods_username: 'Benutzername-und-Passwort Anmeldung',
|
||||
methods_primary_tag: '(Primär)',
|
||||
define_social_methods: 'Definiere die unterstützten Social Anmeldemethoden',
|
||||
transfer: {
|
||||
title: 'Social Connectoren',
|
||||
footer: {
|
||||
not_in_list: 'Nicht in der Liste?',
|
||||
set_up_more: 'Mehr Social Connectoren einrichten',
|
||||
go_to: 'oder "Connectoren" aufrufen.',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: 'NUTZUNGSBEDINGUNGEN',
|
||||
enable: 'Aktiviere Nutzungsbedingungen',
|
||||
description: 'Füge die rechtlichen Vereinbarungen für die Nutzung deines Produkts hinzu',
|
||||
terms_of_use: 'Nutzungsbedingungen',
|
||||
terms_of_use_placeholder: 'https://beispiel.de/nutzungsbedingungen',
|
||||
terms_of_use_tip: 'URL zu den Nutzungsbedingungen',
|
||||
},
|
||||
languages: {
|
||||
title: 'SPRACHEN',
|
||||
enable_auto_detect: 'Aktiviere automatische Spracherkennung',
|
||||
description:
|
||||
'Deine Software erkennt die Sprach-Einstellung des Nutzers und schaltet auf die lokale Sprache um. Du kannst neue Sprachen hinzufügen, indem du die Benutzeroberfläche vom Englischen in eine andere Sprache übersetzt.',
|
||||
manage_language: 'Sprachen verwalten',
|
||||
default_language: 'Standard-Sprache',
|
||||
default_language_description_auto:
|
||||
'Die Standardsprache wird verwendet, wenn die erkannte Benutzersprache nicht in der aktuellen Sprachbibliothek enthalten ist.',
|
||||
default_language_description_fixed:
|
||||
'Wenn die automatische Erkennung deaktiviert ist, ist die Standardsprache die einzige Sprache, die deine Software anzeigt. Schalte die automatische Erkennung ein um weitere Sprachen anzuzeigen.',
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Sprachen verwalten',
|
||||
subtitle:
|
||||
'Erweitere die Anmeldeoberfläche durch neue Sprachen und Übersetzungen. Deine Übersetzung kann als Standard-Sprache verwendet werden.',
|
||||
add_language: 'Sprache hinzufügen',
|
||||
logto_provided: 'Von Logto bereitgestellt',
|
||||
key: 'Schlüssel',
|
||||
logto_source_values: 'Logto Übersetzungen',
|
||||
custom_values: 'Benutzerdefinierte Übersetzungen',
|
||||
clear_all_tip: 'Alle benutzerdefinierten Übersetzungen löschen',
|
||||
unsaved_description:
|
||||
'Wenn du diese Seite verlässt, ohne zu speichern, werden die Änderungen nicht gespeichert.',
|
||||
deletion_tip: 'Sprache löschen',
|
||||
deletion_title: 'Willst du diese Sprache wirklich löschen?',
|
||||
deletion_description:
|
||||
'Nach dem Löschen können deine Benutzer diese Sprache nicht mehr nutzen.',
|
||||
default_language_deletion_title: 'Die Standardsprache kann nicht gelöscht werden.',
|
||||
default_language_deletion_description:
|
||||
'{{language}} ist als Standardsprache eingestellt und kann nicht gelöscht werden. ',
|
||||
got_it: 'Alles klar',
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTIFIZIERUNG',
|
||||
enable_create_account: 'Aktiviere Registrierung',
|
||||
enable_create_account_description:
|
||||
'Aktiviere oder deaktiviere Konto Registrierung. Wenn diese Funktion deaktiviert ist, können deine Kunden keine Konten über die Anmeldeoberfläche erstellen, aber du kannst immer noch Benutzer in der Admin Konsole hinzufügen.',
|
||||
enable_user_registration: 'Enable user registration', // UNTRANSLATED
|
||||
enable_user_registration_description:
|
||||
'Enable or disallow user registration. Once disabled, users can still be added in the admin console but users can no longer establish accounts through the sign-in UI.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'Du hast noch keinen SMS Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
no_connector_email:
|
||||
'Du hast noch keinen E-Mail Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
no_connector_social:
|
||||
'Du hast noch keinen Social Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
no_added_social_connector:
|
||||
'Du hast jetzt ein paar Social Connectoren eingerichtet. Füge jetzt einige zu deinem Anmeldeerlebnis hinzu.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'Du änderst die Anmeldemethoden. Das wird sich auf einige deiner Benutzer auswirken. Bist du sicher, dass du das tun willst?',
|
||||
before: 'Vorher',
|
||||
after: 'Nachher',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
title: 'Vorschau',
|
||||
dark: 'Dunkel',
|
||||
light: 'Hell',
|
||||
native: 'Nativ',
|
||||
desktop_web: 'Desktop Web',
|
||||
mobile_web: 'Mobil Web',
|
||||
},
|
||||
};
|
||||
|
||||
export default sign_in_exp;
|
|
@ -0,0 +1,8 @@
|
|||
const tab_sections = {
|
||||
overview: 'Übersicht',
|
||||
resource_management: 'Ressourcenverwaltung',
|
||||
user_management: 'Benutzerverwaltung',
|
||||
help_and_support: 'Hilfe und Support',
|
||||
};
|
||||
|
||||
export default tab_sections;
|
|
@ -0,0 +1,15 @@
|
|||
const tabs = {
|
||||
get_started: 'Erste Schritte',
|
||||
dashboard: 'Dashboard',
|
||||
applications: 'Anwendungen',
|
||||
api_resources: 'API Ressourcen',
|
||||
sign_in_experience: 'Anmeldeoberfläche',
|
||||
connectors: 'Connectoren',
|
||||
users: 'Benutzerverwaltung',
|
||||
audit_logs: 'Audit Logs',
|
||||
docs: 'Dokumentation',
|
||||
contact_us: 'Kontakt',
|
||||
settings: 'Einstellungen',
|
||||
};
|
||||
|
||||
export default tabs;
|
|
@ -0,0 +1,41 @@
|
|||
const user_details = {
|
||||
back_to_users: 'Zurück zur Benutzerverwaltung',
|
||||
created_title: 'Der Benutzer wurde erfolgreich erstellt',
|
||||
created_guide: 'Sende dem Benutzer folgende Anmeldeinformationen',
|
||||
created_username: 'Benutzername:',
|
||||
created_password: 'Passwort:',
|
||||
menu_delete: 'Löschen',
|
||||
delete_description:
|
||||
'Diese Aktion kann nicht rückgängig gemacht werden. Der Benutzer wird permanent gelöscht.',
|
||||
deleted: 'Der Benutzer wurde erfolgreich gelöscht',
|
||||
reset_password: {
|
||||
reset_password: 'Passwort zurücksetzen',
|
||||
title: 'Willst du das Passwort wirklich zurücksetzen?',
|
||||
content:
|
||||
'Diese Aktion kann nicht rückgängig gemacht werden. Das Anmeldeinformationen werden zurückgesetzt.',
|
||||
congratulations: 'Der Benutzer wurde erfolgreich zurückgesetzt',
|
||||
new_password: 'Neues Passwort:',
|
||||
},
|
||||
tab_logs: 'Benutzer-Logs',
|
||||
field_email: 'Primäre E-Mail',
|
||||
field_phone: 'Primäre Telefonnummer',
|
||||
field_username: 'Benutzername',
|
||||
field_name: 'Name',
|
||||
field_avatar: 'Profilbild URL',
|
||||
field_avatar_placeholder: 'https://dein.cdn.domain/profilbild.png',
|
||||
field_custom_data: 'Benutzerdefinierte Daten',
|
||||
field_custom_data_tip:
|
||||
'Zusätzliche Benutzerinformationen, die nicht in den vordefinierten Benutzereigenschaften aufgeführt sind, wie z. B. die vom Benutzer bevorzugte Farbe und Sprache.',
|
||||
field_connectors: 'Social Connections',
|
||||
custom_data_invalid: 'Benutzerdefinierte Daten müssen ein gültiges JSON-Objekt sein.',
|
||||
connectors: {
|
||||
connectors: 'Connectoren',
|
||||
user_id: 'Benutzer ID',
|
||||
remove: 'Löschen',
|
||||
not_connected: 'Der Nutzer ist nicht mit einem Social Connector verbunden',
|
||||
deletion_confirmation:
|
||||
'Du entfernst die bestehende <name/> Identität. Bist du sicher, dass du das tun willst?',
|
||||
},
|
||||
};
|
||||
|
||||
export default user_details;
|
|
@ -0,0 +1,15 @@
|
|||
const users = {
|
||||
title: 'Benutzerverwaltung',
|
||||
subtitle:
|
||||
'Verwalten von Benutzeridentitäten, einschließlich des Anlegens von Benutzern, Bearbeiten von Benutzerinformationen, Anzeigen von Benutzer-Logs, Zurücksetzen von Passwörtern und Löschen von Benutzern',
|
||||
create: 'Benutzer hinzufügen',
|
||||
user_name: 'Benutzer',
|
||||
application_name: 'Anwendungsname',
|
||||
latest_sign_in: 'Letzte Anmeldung',
|
||||
create_form_username: 'Benutzername',
|
||||
create_form_password: 'Passwort',
|
||||
create_form_name: 'Name',
|
||||
unnamed: 'Unbenannt',
|
||||
};
|
||||
|
||||
export default users;
|
|
@ -0,0 +1,8 @@
|
|||
const welcome = {
|
||||
title: 'Willkommen in der Admin Konsole',
|
||||
description:
|
||||
'Die Admin-Konsole ist eine Web-App, mit der du Logto verwalten kannst, ohne programmieren zu müssen. Legen wir zunächst ein Konto an. Mit diesem Konto kannst du Logto selbst oder im Namen deines Unternehmens verwalten.',
|
||||
create_account: 'Konto erstellen',
|
||||
};
|
||||
|
||||
export default welcome;
|
15
packages/phrases/src/locales/de/translation/demo-app.ts
Normal file
15
packages/phrases/src/locales/de/translation/demo-app.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
const demo_app = {
|
||||
notification:
|
||||
'Nutze dein existierendes Admin Konto oder erstelle ein neues Konto um dich in die Demo App einzuloggen.',
|
||||
title: 'Du hast dich erfolgreich in der Demo App angemeldet!',
|
||||
subtitle: 'Here is your log in information:',
|
||||
username: 'Benutzername: ',
|
||||
user_id: 'Benutzer ID: ',
|
||||
sign_out: 'Aus der Demo App ausloggen',
|
||||
continue_explore: 'Oder weiter zum Entdecken',
|
||||
customize_sign_in_experience: 'Anmeldeoberfläche anpassen',
|
||||
enable_passwordless: 'Passwordless einschalten',
|
||||
add_social_connector: 'Social Connector hinzufügen',
|
||||
};
|
||||
|
||||
export default demo_app;
|
9
packages/phrases/src/locales/de/translation/index.ts
Normal file
9
packages/phrases/src/locales/de/translation/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import admin_console from './admin-console';
|
||||
import demo_app from './demo-app';
|
||||
|
||||
const translation = {
|
||||
admin_console,
|
||||
demo_app,
|
||||
};
|
||||
|
||||
export default translation;
|
|
@ -27,7 +27,7 @@
|
|||
"test:ci": "jest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand",
|
||||
|
@ -45,7 +45,6 @@
|
|||
"dependencies": {
|
||||
"@logto/schemas": "workspace:^",
|
||||
"@silverhand/essentials": "^1.3.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"find-up": "^5.0.0",
|
||||
"nanoid": "^3.3.4",
|
||||
"slonik": "^30.0.0"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { sql } from 'slonik';
|
||||
import { SqlToken } from 'slonik/dist/src/tokens.js';
|
||||
|
||||
|
@ -124,7 +123,7 @@ describe('convertToTimestamp()', () => {
|
|||
});
|
||||
|
||||
it('converts to sql per time parameter', () => {
|
||||
const time = dayjs(123_123_123);
|
||||
const time = new Date(123_123_123);
|
||||
|
||||
expect(convertToTimestamp(time)).toEqual({
|
||||
sql: 'to_timestamp($1)',
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { SchemaValuePrimitive, SchemaValue } from '@logto/schemas';
|
||||
import type { Falsy } from '@silverhand/essentials';
|
||||
import { notFalsy } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import type { SqlSqlToken, SqlToken, QueryResult, IdentifierSqlToken } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
|
@ -76,7 +75,8 @@ export const convertToIdentifiers = <T extends Table>({ table, fields }: T, with
|
|||
};
|
||||
};
|
||||
|
||||
export const convertToTimestamp = (time = dayjs()) => sql`to_timestamp(${time.valueOf() / 1000})`;
|
||||
export const convertToTimestamp = (time = new Date()) =>
|
||||
sql`to_timestamp(${time.valueOf() / 1000})`;
|
||||
|
||||
export const manyRows = async <T>(query: Promise<QueryResult<T>>): Promise<readonly T[]> => {
|
||||
const { rows } = await query;
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
"i18next": "^21.8.16",
|
||||
"i18next-browser-languagedetector": "^6.1.4",
|
||||
"jest": "^29.1.2",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-transformer-svg": "^2.0.0",
|
||||
"js-base64": "^3.7.2",
|
||||
"ky": "^0.31.0",
|
||||
|
@ -69,6 +69,17 @@
|
|||
"typescript": "^4.7.4",
|
||||
"use-debounced-loader": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"//": "https://github.com/parcel-bundler/parcel/issues/7636",
|
||||
"targets": {
|
||||
"default": {
|
||||
"engines": {
|
||||
"browsers": "defaults"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alias": {
|
||||
"@/*": "./src/$1"
|
||||
},
|
||||
|
|
793
pnpm-lock.yaml
generated
793
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue