From 682c88aac80588a31e8fc61c4b96ac9f9c935bea Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 7 Nov 2022 11:47:51 +0800 Subject: [PATCH 01/32] feat(cli): rotate keys --- packages/cli/src/commands/database/config.ts | 64 ++++++++++++++++++- .../src/commands/database/seed/oidc-config.ts | 22 ++----- .../cli/src/commands/database/utilities.ts | 22 +++++++ 3 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 packages/cli/src/commands/database/utilities.ts diff --git a/packages/cli/src/commands/database/config.ts b/packages/cli/src/commands/database/config.ts index 1ac8f55c5..e503701f6 100644 --- a/packages/cli/src/commands/database/config.ts +++ b/packages/cli/src/commands/database/config.ts @@ -1,5 +1,5 @@ import type { LogtoConfigKey } from '@logto/schemas'; -import { logtoConfigGuards, logtoConfigKeys } from '@logto/schemas'; +import { LogtoOidcConfigKey, logtoConfigGuards, logtoConfigKeys } from '@logto/schemas'; import { deduplicate, noop } from '@silverhand/essentials'; import chalk from 'chalk'; import type { CommandModule } from 'yargs'; @@ -7,6 +7,7 @@ import type { CommandModule } from 'yargs'; import { createPoolFromConfig } from '../../database'; import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config'; import { log } from '../../utilities'; +import { generateOidcCookieKey, generateOidcPrivateKey } from './utilities'; const validKeysDisplay = chalk.green(logtoConfigKeys.join(', ')); @@ -29,6 +30,21 @@ const validateKeys: ValidateKeysFunction = (keys) => { } }; +const validRotateKeys = Object.freeze([ + LogtoOidcConfigKey.PrivateKeys, + LogtoOidcConfigKey.CookieKeys, +] as const); + +type ValidateRotateKeyFunction = (key: string) => asserts key is typeof validRotateKeys[number]; + +const validateRotateKey: ValidateRotateKeyFunction = (key) => { + // Using `.includes()` will result a type error + // eslint-disable-next-line unicorn/prefer-includes + if (!validRotateKeys.some((element) => element === key)) { + log.error(`Invalid config key ${chalk.red(key)} found, expected one of ${validKeysDisplay}`); + } +}; + const getConfig: CommandModule = { command: 'get [keys...]', describe: 'Get config value(s) of the given key(s) in Logto database', @@ -97,10 +113,54 @@ const setConfig: CommandModule = { }, }; +const rotateConfig: CommandModule = { + command: 'rotate ', + describe: + 'Generate a new private or secret key for the given config key and prepend to the key array', + builder: (yargs) => + yargs.positional('key', { + describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`, + type: 'string', + demandOption: true, + }), + handler: async ({ key }) => { + validateRotateKey(key); + + const pool = await createPoolFromConfig(); + const { rows } = await getRowsByKeys(pool, [key]); + + if (!rows[0]) { + log.warn('No key found, create a new one'); + } + + const getValue = async () => { + const parsed = logtoConfigGuards[key].safeParse(rows[0]?.value); + const original = parsed.success ? parsed.data : []; + + switch (key) { + case LogtoOidcConfigKey.PrivateKeys: + return [await generateOidcPrivateKey(), ...original]; + case LogtoOidcConfigKey.CookieKeys: + return [generateOidcCookieKey(), ...original]; + default: + log.warn('No proper handler found, use empty array'); + + return []; + } + }; + const rotated = await getValue(); + await updateValueByKey(pool, key, rotated); + await pool.end(); + + log.info(`Rotate ${chalk.green(key)} succeeded, now it has ${rotated.length} keys`); + }, +}; + const config: CommandModule = { command: ['config', 'configs'], describe: 'Commands for Logto database config', - builder: (yargs) => yargs.command(getConfig).command(setConfig).demandCommand(1), + builder: (yargs) => + yargs.command(getConfig).command(setConfig).command(rotateConfig).demandCommand(1), handler: noop, }; diff --git a/packages/cli/src/commands/database/seed/oidc-config.ts b/packages/cli/src/commands/database/seed/oidc-config.ts index 4e4678263..a65b19c29 100644 --- a/packages/cli/src/commands/database/seed/oidc-config.ts +++ b/packages/cli/src/commands/database/seed/oidc-config.ts @@ -1,11 +1,10 @@ -import { generateKeyPair } from 'crypto'; import { readFile } from 'fs/promises'; -import { promisify } from 'util'; import type { LogtoOidcConfigType } from '@logto/schemas'; import { LogtoOidcConfigKey } from '@logto/schemas'; import { getEnv, getEnvAsStringArray } from '@silverhand/essentials'; -import { nanoid } from 'nanoid'; + +import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities'; const isBase64FormatPrivateKey = (key: string) => !key.includes('-'); @@ -57,21 +56,8 @@ export const oidcConfigReaders: { }; } - // Generate a new key - const { privateKey } = await promisify(generateKeyPair)('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - }, - }); - return { - value: [privateKey], + value: [await generateOidcPrivateKey()], fromEnv: false, }; }, @@ -79,7 +65,7 @@ export const oidcConfigReaders: { const envKey = 'OIDC_COOKIE_KEYS'; const keys = getEnvAsStringArray(envKey); - return { value: keys.length > 0 ? keys : [nanoid()], fromEnv: keys.length > 0 }; + return { value: keys.length > 0 ? keys : [generateOidcCookieKey()], fromEnv: keys.length > 0 }; }, [LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => { const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL'; diff --git a/packages/cli/src/commands/database/utilities.ts b/packages/cli/src/commands/database/utilities.ts new file mode 100644 index 000000000..84c4f2ac8 --- /dev/null +++ b/packages/cli/src/commands/database/utilities.ts @@ -0,0 +1,22 @@ +import { generateKeyPair } from 'crypto'; +import { promisify } from 'util'; + +import { nanoid } from 'nanoid'; + +export const generateOidcPrivateKey = async () => { + const { privateKey } = await promisify(generateKeyPair)('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + + return privateKey; +}; + +export const generateOidcCookieKey = () => nanoid(); From cf2c546f5d1ccc0a70846a820566b98529fd38c6 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 7 Nov 2022 11:53:02 +0800 Subject: [PATCH 02/32] refactor(cli): remove default case --- packages/cli/src/commands/database/config.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/database/config.ts b/packages/cli/src/commands/database/config.ts index e503701f6..9f6c6c540 100644 --- a/packages/cli/src/commands/database/config.ts +++ b/packages/cli/src/commands/database/config.ts @@ -137,15 +137,13 @@ const rotateConfig: CommandModule = { const parsed = logtoConfigGuards[key].safeParse(rows[0]?.value); const original = parsed.success ? parsed.data : []; + // No need for default. It's already exhaustive + // eslint-disable-next-line default-case switch (key) { case LogtoOidcConfigKey.PrivateKeys: return [await generateOidcPrivateKey(), ...original]; case LogtoOidcConfigKey.CookieKeys: return [generateOidcCookieKey(), ...original]; - default: - log.warn('No proper handler found, use empty array'); - - return []; } }; const rotated = await getValue(); From 3ff2e90cd3289415a1a49ec235f05523b0f2ec34 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 7 Nov 2022 16:53:08 +0800 Subject: [PATCH 03/32] chore: add changeset for rotate via CLI --- .changeset/unlucky-lizards-agree.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/unlucky-lizards-agree.md diff --git a/.changeset/unlucky-lizards-agree.md b/.changeset/unlucky-lizards-agree.md new file mode 100644 index 000000000..3e5bf43eb --- /dev/null +++ b/.changeset/unlucky-lizards-agree.md @@ -0,0 +1,13 @@ +--- +"@logto/cli": minor +--- + +## CLI + +### Rotate your private or secret key + +Add a new command `db config rotate ` to support key rotation via CLI. + +When rotating, the CLI will generate a new key and prepend to the corresponding key array. Thus the old key is still valid and the serivce will use the new key for signing. + +Run `logto db config rotate help` for detailed usage. From 3585a77de961db503e50888e3a0d15ede711480e Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 7 Nov 2022 19:26:56 +0800 Subject: [PATCH 04/32] feat(cli): trim keys --- .changeset/unlucky-lizards-agree.md | 14 ++++- packages/cli/src/commands/database/config.ts | 55 +++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.changeset/unlucky-lizards-agree.md b/.changeset/unlucky-lizards-agree.md index 3e5bf43eb..af458f4f3 100644 --- a/.changeset/unlucky-lizards-agree.md +++ b/.changeset/unlucky-lizards-agree.md @@ -6,8 +6,18 @@ ### Rotate your private or secret key -Add a new command `db config rotate ` to support key rotation via CLI. +We add a new command `db config rotate ` to support key rotation via CLI. -When rotating, the CLI will generate a new key and prepend to the corresponding key array. Thus the old key is still valid and the serivce will use the new key for signing. +When rotating, the CLI will generate a new key and prepend to the corresponding key array. Thus the old key is still valid and the service will use the new key for signing. Run `logto db config rotate help` for detailed usage. + +### Trim the private or secret key you don't need + +If you want to trim one or more out-dated private or secret key(s) from the config, use the command `db config trim `. It will remove the last item (private or secret key) in the array. + +You may remove the old key after a certain period (such as half a year) to allow most of your users have time to touch the new key. + +If you want to remove multiple keys at once, just append a number to the command. E.g. `logto db config trim oidc.cookieKeys 3`. + +Run `logto db config trim help` for detailed usage. diff --git a/packages/cli/src/commands/database/config.ts b/packages/cli/src/commands/database/config.ts index 9f6c6c540..cb61e1fa5 100644 --- a/packages/cli/src/commands/database/config.ts +++ b/packages/cli/src/commands/database/config.ts @@ -154,11 +154,64 @@ const rotateConfig: CommandModule = { }, }; +const trimConfig: CommandModule = { + command: 'trim [length]', + describe: 'Remove the last [length] number of private or secret keys for the given config key', + builder: (yargs) => + yargs + .positional('key', { + describe: `The config key to trim, one of ${chalk.green(validRotateKeys.join(', '))}`, + type: 'string', + demandOption: true, + }) + .positional('length', { + describe: 'Number of private or secret keys to trim', + type: 'number', + default: 1, + demandOption: true, + }), + handler: async ({ key, length }) => { + validateRotateKey(key); + + if (length < 1) { + log.error('Invalid length provided'); + } + + const pool = await createPoolFromConfig(); + const { rows } = await getRowsByKeys(pool, [key]); + + if (!rows[0]) { + log.warn('No key found, create a new one'); + } + + const getValue = async () => { + const value = logtoConfigGuards[key].parse(rows[0]?.value); + + if (value.length - length < 1) { + await pool.end(); + log.error(`You should keep at least one key in the array, current length=${value.length}`); + } + + return value.slice(0, -length); + }; + const trimmed = await getValue(); + await updateValueByKey(pool, key, trimmed); + await pool.end(); + + log.info(`Trim ${chalk.green(key)} succeeded, now it has ${trimmed.length} keys`); + }, +}; + const config: CommandModule = { command: ['config', 'configs'], describe: 'Commands for Logto database config', builder: (yargs) => - yargs.command(getConfig).command(setConfig).command(rotateConfig).demandCommand(1), + yargs + .command(getConfig) + .command(setConfig) + .command(rotateConfig) + .command(trimConfig) + .demandCommand(1), handler: noop, }; From 960fbc38c46267c9c4c0e7d0f293977aaa05d606 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 9 Nov 2022 15:03:48 +0800 Subject: [PATCH 05/32] feat(console): add toggle tip for the sie not-in-use status (#2323) --- .../ConnectorStatusField/index.module.scss | 28 ++++++++ .../components/ConnectorStatusField/index.tsx | 64 +++++++++++++++++++ .../console/src/pages/Connectors/index.tsx | 5 +- .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ .../translation/admin-console/connectors.ts | 5 ++ 10 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss create mode 100644 packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss new file mode 100644 index 000000000..e70a6b512 --- /dev/null +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss @@ -0,0 +1,28 @@ +@use '@/scss/underscore' as _; + +.field { + display: flex; + align-items: center; +} + +.tipIcon { + margin-left: _.unit(1); + + > svg { + display: block; + cursor: pointer; + } +} + +.title { + font: var(--font-label-large); +} + +.content { + font: var(--font-body-medium); + + a { + color: #cabeff; + text-decoration: none; + } +} diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx new file mode 100644 index 000000000..c626fb465 --- /dev/null +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx @@ -0,0 +1,64 @@ +import { useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import Tip from '@/assets/images/tip.svg'; +import ToggleTip from '@/components/ToggleTip'; +import { onKeyDownHandler } from '@/utilities/a11y'; + +import * as styles from './index.module.scss'; + +const ConnectorStatusField = () => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const [isTipOpen, setIsTipOpen] = useState(false); + const anchorRef = useRef(null); + + return ( +
+ {t('connectors.connector_status')} +
+ { + setIsTipOpen(true); + }} + onKeyDown={onKeyDownHandler(() => { + setIsTipOpen(true); + })} + /> +
+ { + setIsTipOpen(false); + }} + > +
{t('connectors.connector_status')}
+
+ { + setIsTipOpen(false); + }} + /> + ), + }} + > + {t('connectors.not_in_use_tip.content', { + link: t('connectors.not_in_use_tip.go_to_sie'), + })} + +
+
+
+ ); +}; + +export default ConnectorStatusField; diff --git a/packages/console/src/pages/Connectors/index.tsx b/packages/console/src/pages/Connectors/index.tsx index e87cfe0d4..d57fd09eb 100644 --- a/packages/console/src/pages/Connectors/index.tsx +++ b/packages/console/src/pages/Connectors/index.tsx @@ -19,6 +19,7 @@ import { useTheme } from '@/hooks/use-theme'; import * as tableStyles from '@/scss/table.module.scss'; import ConnectorRow from './components/ConnectorRow'; +import ConnectorStatusField from './components/ConnectorStatusField'; import CreateForm from './components/CreateForm'; import SignInExperienceSetupNotice from './components/SignInExperienceSetupNotice'; import * as styles from './index.module.scss'; @@ -90,7 +91,9 @@ const Connectors = () => { {t('connectors.connector_name')} {t('connectors.connector_type')} - {t('connectors.connector_status')} + + + diff --git a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts index 63c7b3117..012a55a52 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts @@ -11,6 +11,11 @@ const connectors = { connector_status: 'Anmeldeoberfläche', connector_status_in_use: 'In Benutzung', connector_status_not_in_use: 'Nicht in Benutzung', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: 'z.B. Google, Facebook, Github', save_and_done: 'Speichern und fertigstellen', type: { diff --git a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts index 075d02e24..993947c0a 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts @@ -11,6 +11,11 @@ const connectors = { connector_status: 'Sign in Experience', connector_status_in_use: 'In use', connector_status_not_in_use: 'Not in use', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', + go_to_sie: 'Go to sign in experience', + }, social_connector_eg: 'E.g., Google, Facebook, Github', save_and_done: 'Save and Done', type: { diff --git a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts index 88bb1ecf8..894bb377f 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts @@ -12,6 +12,11 @@ const connectors = { connector_status: 'Experience de connexion', connector_status_in_use: "En cours d'utilisation", connector_status_not_in_use: 'Non utilisé', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: 'Exemple : Google, Facebook, Github', save_and_done: 'Sauvegarder et Finis', type: { diff --git a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts index 1e77ad925..f6936ee0e 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts @@ -11,6 +11,11 @@ const connectors = { connector_status: '로그인 경험', connector_status_in_use: '사용 중', connector_status_not_in_use: '사용 중이 아님', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: '예) Google, Facebook, Github', save_and_done: '저장 및 완료', type: { diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts index 62a8b604d..2499f0edf 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts @@ -11,6 +11,11 @@ const connectors = { connector_status: 'Experiência de login', connector_status_in_use: 'Em uso', connector_status_not_in_use: 'Fora de uso', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: 'Ex., Google, Facebook, Github', save_and_done: 'Guardar', type: { diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts index 4a3d3b155..92c226579 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts @@ -12,6 +12,11 @@ const connectors = { connector_status: 'Oturum açma deneyimi', connector_status_in_use: 'Kullanımda', connector_status_not_in_use: 'Kullanımda değil', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: 'Örneğin, Google, Facebook, Github', save_and_done: 'Kaydet ve bitir', type: { diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts index 26b698bd3..c2f6b5089 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts @@ -11,6 +11,11 @@ const connectors = { connector_status: '登录体验', connector_status_in_use: '使用中', connector_status_not_in_use: '未使用', + not_in_use_tip: { + content: + 'Not in use means your sign in experience hasn’t used this sign in method. {{link}} to add this sign in method. ', // UNTRANSLATED + go_to_sie: 'Go to sign in experience', // UNTRANSLATED + }, social_connector_eg: '如: 微信登录,支付宝登录', save_and_done: '保存并完成', type: { From ec36266a1b23442b4d83191bf9bd3dd7278ff68b Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 9 Nov 2022 15:10:37 +0800 Subject: [PATCH 06/32] feat(core): add sign-in timestamp in session interaction (#2325) --- packages/core/src/lib/session.ts | 24 +++++++++++++++---- .../core/src/routes/session/continue.test.ts | 12 ++++++---- .../core/src/routes/session/password.test.ts | 12 ++++++---- .../src/routes/session/passwordless.test.ts | 24 ++++++++++++------- .../core/src/routes/session/social.test.ts | 16 +++++++++---- .../core/src/routes/session/utils.test.ts | 3 ++- 6 files changed, 65 insertions(+), 26 deletions(-) diff --git a/packages/core/src/lib/session.ts b/packages/core/src/lib/session.ts index e55d13001..86ff51ba4 100644 --- a/packages/core/src/lib/session.ts +++ b/packages/core/src/lib/session.ts @@ -1,3 +1,5 @@ +import { conditional } from '@silverhand/essentials'; +import { getUnixTime } from 'date-fns'; import type { Context } from 'koa'; import type { InteractionResults, Provider } from 'oidc-provider'; @@ -14,20 +16,32 @@ export const assignInteractionResults = async ( // have to do it manually // refer to: https://github.com/panva/node-oidc-provider/blob/c243bf6b6663c41ff3e75c09b95fb978eba87381/lib/actions/authorization/interactions.js#L106 const details = merge ? await provider.interactionDetails(ctx.req, ctx.res) : undefined; - + const ts = getUnixTime(new Date()); + const mergedResult = { + // Merge with current result + ...details?.result, + ...result, + }; const redirectTo = await provider.interactionResult( ctx.req, ctx.res, { - // Merge with current result - ...details?.result, - ...result, + ...mergedResult, + ...conditional( + mergedResult.login && { + login: { + ...mergedResult.login, + // Update ts(timestamp) if the accountId has been set in result + ts: result.login?.accountId ? ts : mergedResult.login.ts, + }, + } + ), }, { mergeWithLastSubmission: merge, } ); - ctx.body = { redirectTo }; + ctx.body = { redirectTo, ts }; }; export const saveUserFirstConsentedAppId = async (userId: string, applicationId: string) => { diff --git a/packages/core/src/routes/session/continue.test.ts b/packages/core/src/routes/session/continue.test.ts index 76d4456fe..05ae2f17b 100644 --- a/packages/core/src/routes/session/continue.test.ts +++ b/packages/core/src/routes/session/continue.test.ts @@ -89,7 +89,8 @@ describe('session -> continueRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -121,7 +122,8 @@ describe('session -> continueRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -156,7 +158,8 @@ describe('session -> continueRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -188,7 +191,8 @@ describe('session -> continueRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); diff --git a/packages/core/src/routes/session/password.test.ts b/packages/core/src/routes/session/password.test.ts index a3c0af8c8..212cd7645 100644 --- a/packages/core/src/routes/session/password.test.ts +++ b/packages/core/src/routes/session/password.test.ts @@ -111,7 +111,8 @@ describe('session -> password routes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'id' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), expect.anything() ); }); @@ -127,7 +128,8 @@ describe('session -> password routes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'id' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), expect.anything() ); }); @@ -143,7 +145,8 @@ describe('session -> password routes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'id' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), expect.anything() ); }); @@ -172,7 +175,8 @@ describe('session -> password routes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'user1' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'user1', ts: expect.any(Number) } }), expect.anything() ); jest.useRealTimers(); diff --git a/packages/core/src/routes/session/passwordless.test.ts b/packages/core/src/routes/session/passwordless.test.ts index 47dc04c74..77d428e20 100644 --- a/packages/core/src/routes/session/passwordless.test.ts +++ b/packages/core/src/routes/session/passwordless.test.ts @@ -392,7 +392,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: mockUser.id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: mockUser.id, ts: expect.any(Number) }, }), expect.anything() ); @@ -414,7 +415,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: mockUser.id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: mockUser.id, ts: expect.any(Number) }, }), expect.anything() ); @@ -555,7 +557,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: mockUser.id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: mockUser.id, ts: expect.any(Number) }, }), expect.anything() ); @@ -579,7 +582,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: mockUser.id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: mockUser.id, ts: expect.any(Number) }, }), expect.anything() ); @@ -688,7 +692,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: 'user1' }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: 'user1', ts: expect.any(Number) }, }), expect.anything() ); @@ -710,7 +715,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: 'user1' }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: 'user1', ts: expect.any(Number) }, }), expect.anything() ); @@ -816,7 +822,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: 'user1' }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: 'user1', ts: expect.any(Number) }, }), expect.anything() ); @@ -838,7 +845,8 @@ describe('session -> passwordlessRoutes', () => { expect.anything(), expect.anything(), expect.objectContaining({ - login: { accountId: 'user1' }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + login: { accountId: 'user1', ts: expect.any(Number) }, }), expect.anything() ); diff --git a/packages/core/src/routes/session/social.test.ts b/packages/core/src/routes/session/social.test.ts index e00a6a8cd..d67f2b3f9 100644 --- a/packages/core/src/routes/session/social.test.ts +++ b/packages/core/src/routes/session/social.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { ConnectorType } from '@logto/connector-kit'; import type { User } from '@logto/schemas'; import { SignUpIdentifier } from '@logto/schemas'; @@ -233,7 +234,8 @@ describe('session -> socialRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -316,7 +318,8 @@ describe('session -> socialRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'user1' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'user1', ts: expect.any(Number) } }), expect.anything() ); }); @@ -350,7 +353,8 @@ describe('session -> socialRoutes', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: 'user1' } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: 'user1', ts: expect.any(Number) } }), expect.anything() ); }); @@ -364,7 +368,10 @@ describe('session -> socialRoutes', () => { }); it('throw error if result parsing fails', async () => { - interactionDetails.mockResolvedValueOnce({ result: { login: { accountId: mockUser.id } } }); + interactionDetails.mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + result: { login: { accountId: mockUser.id, ts: expect.any(Number) } }, + }); const response = await sessionRequest .post(`${registerRoute}`) .send({ connectorId: 'connectorId' }); @@ -436,3 +443,4 @@ describe('session -> socialRoutes', () => { }); }); }); +/* eslint-enable max-lines */ diff --git a/packages/core/src/routes/session/utils.test.ts b/packages/core/src/routes/session/utils.test.ts index 3bceb3391..49d6e2255 100644 --- a/packages/core/src/routes/session/utils.test.ts +++ b/packages/core/src/routes/session/utils.test.ts @@ -121,7 +121,8 @@ describe('signInWithPassword()', () => { expect(interactionResult).toHaveBeenCalledWith( expect.anything(), expect.anything(), - expect.objectContaining({ login: { accountId: mockUser.id } }), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); From 605161b8d243b09ff6a52c5564bb19a18a652975 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 9 Nov 2022 15:25:32 +0800 Subject: [PATCH 07/32] refactor(console): remove draggle preview item background (#2364) --- .../console/src/components/Transfer/DraggableItem.tsx | 5 +++-- .../components/SignInMethodEditBox/index.module.scss | 4 ++++ .../components/SignInMethodEditBox/index.tsx | 2 ++ .../components/SocialConnectorEditBox/index.module.scss | 4 ++++ .../components/SocialConnectorEditBox/index.tsx | 8 +++++++- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/console/src/components/Transfer/DraggableItem.tsx b/packages/console/src/components/Transfer/DraggableItem.tsx index c64a05f0f..98682f662 100644 --- a/packages/console/src/components/Transfer/DraggableItem.tsx +++ b/packages/console/src/components/Transfer/DraggableItem.tsx @@ -11,6 +11,7 @@ type Props = { sortIndex: number; moveItem: (dragIndex: number, hoverIndex: number) => void; children: ReactNode; + className?: string; }; type DragItemProps = { @@ -21,7 +22,7 @@ type DragItemProps = { const dragType = 'TransferItem'; -const DraggableItem = ({ id, children, sortIndex, moveItem }: Props) => { +const DraggableItem = ({ id, children, sortIndex, moveItem, className }: Props) => { const ref = useRef(null); const { setIsDragging } = useContext(DragDropContext); const [{ handlerId }, drop] = useDrop }>({ @@ -99,7 +100,7 @@ const DraggableItem = ({ id, children, sortIndex, moveItem }: Props) => { }, [setIsDragging, isDragging]); return ( -
+
{children}
); diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss index 31111b57a..6cb490336 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss @@ -1,5 +1,9 @@ @use '@/scss/underscore' as _; +.draggleItemContainer { + transform: translate(0, 0); +} + .signInMethodItem { display: flex; align-items: center; diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx index a45d8ada0..86adb772b 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx @@ -9,6 +9,7 @@ import { signInIdentifiers, signInIdentifierToRequiredConnectorMapping } from '. import ConnectorSetupWarning from '../ConnectorSetupWarning'; import AddButton from './AddButton'; import SignInMethodItem from './SignInMethodItem'; +import * as styles from './index.module.scss'; import type { SignInMethod } from './types'; import { computeOnSignInMethodAppended, @@ -125,6 +126,7 @@ const SignInMethodEditBox = ({ id={signInMethod.identifier} sortIndex={index} moveItem={onMoveItem} + className={styles.draggleItemContainer} > {
{selectedConnectorItems.map((item, index) => ( - + { From d015aa934cee6619814b8b3d928e34a6ce798515 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 9 Nov 2022 15:38:07 +0800 Subject: [PATCH 08/32] feat(core,phrases): add re-authentication check function for protected access (#2327) --- packages/core/src/lib/session.ts | 25 ++++++++++++++++++++ packages/phrases/src/locales/de/errors.ts | 1 + packages/phrases/src/locales/en/errors.ts | 1 + packages/phrases/src/locales/fr/errors.ts | 1 + packages/phrases/src/locales/ko/errors.ts | 1 + packages/phrases/src/locales/pt-pt/errors.ts | 1 + packages/phrases/src/locales/tr-tr/errors.ts | 1 + packages/phrases/src/locales/zh-cn/errors.ts | 1 + 8 files changed, 32 insertions(+) diff --git a/packages/core/src/lib/session.ts b/packages/core/src/lib/session.ts index 86ff51ba4..7d2508675 100644 --- a/packages/core/src/lib/session.ts +++ b/packages/core/src/lib/session.ts @@ -3,6 +3,7 @@ import { getUnixTime } from 'date-fns'; import type { Context } from 'koa'; import type { InteractionResults, Provider } from 'oidc-provider'; +import RequestError from '@/errors/RequestError'; import { findUserById, updateUserById } from '@/queries/user'; export const assignInteractionResults = async ( @@ -44,6 +45,30 @@ export const assignInteractionResults = async ( ctx.body = { redirectTo, ts }; }; +export const checkSessionHealth = async ( + ctx: Context, + provider: Provider, + tolerance = 10 * 60 // 10 mins +) => { + const { result } = await provider.interactionDetails(ctx.req, ctx.res); + + if (!result?.login?.accountId) { + throw new RequestError('auth.unauthorized'); + } + + if (!result.login.ts || result.login.ts < getUnixTime(new Date()) - tolerance) { + const { passwordEncrypted, primaryPhone, primaryEmail } = await findUserById( + result.login.accountId + ); + + // No authenticated method configured for this user. Pass! + if (!passwordEncrypted && !primaryPhone && !primaryEmail) { + return; + } + throw new RequestError('auth.require_re_authentication'); + } +}; + export const saveUserFirstConsentedAppId = async (userId: string, applicationId: string) => { const { applicationId: firstConsentedAppId } = await findUserById(userId); diff --git a/packages/phrases/src/locales/de/errors.ts b/packages/phrases/src/locales/de/errors.ts index 48c9e4ec6..0fb3545ca 100644 --- a/packages/phrases/src/locales/de/errors.ts +++ b/packages/phrases/src/locales/de/errors.ts @@ -7,6 +7,7 @@ const errors = { expected_role_not_found: 'Erwartete Rolle nicht gefunden. Bitte überprüfe deine Rollen und Berechtigungen.', jwt_sub_missing: '`sub` fehlt in JWT.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', // UNTRANSLATED }, guard: { invalid_input: 'Die Anfrage {{type}} ist ungültig.', diff --git a/packages/phrases/src/locales/en/errors.ts b/packages/phrases/src/locales/en/errors.ts index 66450f599..79f0add52 100644 --- a/packages/phrases/src/locales/en/errors.ts +++ b/packages/phrases/src/locales/en/errors.ts @@ -7,6 +7,7 @@ const errors = { expected_role_not_found: 'Expected role not found. Please check your user roles and permissions.', jwt_sub_missing: 'Missing `sub` in JWT.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', }, guard: { invalid_input: 'The request {{type}} is invalid.', diff --git a/packages/phrases/src/locales/fr/errors.ts b/packages/phrases/src/locales/fr/errors.ts index 3e81986cc..d96ef481c 100644 --- a/packages/phrases/src/locales/fr/errors.ts +++ b/packages/phrases/src/locales/fr/errors.ts @@ -8,6 +8,7 @@ const errors = { expected_role_not_found: 'Expected role not found. Please check your user roles and permissions.', jwt_sub_missing: '`sub` manquant dans JWT.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', // UNTRANSLATED }, guard: { invalid_input: "La requête {{type}} n'est pas valide.", diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index 0ea74901d..6f6d15043 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -7,6 +7,7 @@ const errors = { expected_role_not_found: 'Expected role not found. Please check your user roles and permissions.', jwt_sub_missing: 'JWT에서 `sub`를 찾을 수 없어요.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', // UNTRANSLATED }, guard: { invalid_input: '{{type}} 요청 타입은 유효하지 않아요.', diff --git a/packages/phrases/src/locales/pt-pt/errors.ts b/packages/phrases/src/locales/pt-pt/errors.ts index 224728440..1b8b58ef3 100644 --- a/packages/phrases/src/locales/pt-pt/errors.ts +++ b/packages/phrases/src/locales/pt-pt/errors.ts @@ -6,6 +6,7 @@ const errors = { forbidden: 'Proibido. Verifique os seus cargos e permissões.', expected_role_not_found: 'Role esperado não encontrado. Verifique os seus cargos e permissões.', jwt_sub_missing: 'Campo `sub` está ausente no JWT.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', // UNTRANSLATED }, guard: { invalid_input: 'O pedido {{type}} é inválido.', diff --git a/packages/phrases/src/locales/tr-tr/errors.ts b/packages/phrases/src/locales/tr-tr/errors.ts index 7234df25a..b3b8269bf 100644 --- a/packages/phrases/src/locales/tr-tr/errors.ts +++ b/packages/phrases/src/locales/tr-tr/errors.ts @@ -7,6 +7,7 @@ const errors = { expected_role_not_found: 'Expected role not found. Please check your user roles and permissions.', jwt_sub_missing: 'JWTde `sub` eksik.', + require_re_authentication: 'Re-authentication is required to perform a protected action.', // UNTRANSLATED }, guard: { invalid_input: 'İstek {{type}} geçersiz.', diff --git a/packages/phrases/src/locales/zh-cn/errors.ts b/packages/phrases/src/locales/zh-cn/errors.ts index 3cdf5263d..fd8e4c270 100644 --- a/packages/phrases/src/locales/zh-cn/errors.ts +++ b/packages/phrases/src/locales/zh-cn/errors.ts @@ -6,6 +6,7 @@ const errors = { forbidden: '禁止访问。请检查用户 role 与权限。', expected_role_not_found: '未找到期望的 role。请检查用户 role 与权限。', jwt_sub_missing: 'JWT 缺失 `sub`', + require_re_authentication: '需要重新认证以进行受保护操作。', }, guard: { invalid_input: '请求中 {{type}} 无效', From ac97fb75b04c1f883671a155d8e2eda541070ce1 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 9 Nov 2022 15:52:29 +0800 Subject: [PATCH 09/32] refactor(console): enabled verification code checkbox when sign-up identifier set to none (#2360) --- .../components/SignInMethodEditBox/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx index 86adb772b..8d94cc994 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.tsx @@ -134,9 +134,7 @@ const SignInMethodEditBox = ({ signInMethod.identifier !== SignInIdentifier.Username && !isSignUpPasswordRequired } isVerificationCodeCheckable={ - (isSignUpPasswordRequired && isSignUpVerificationRequired) || - // Note: the next line is used to handle the case when the sign-up identifier is `Username` - (isSignUpPasswordRequired && signInMethod.identifier !== SignInIdentifier.Username) + !(isSignUpVerificationRequired && !isSignUpPasswordRequired) } isDeletable={!requiredSignInIdentifiers.includes(signInMethod.identifier)} onVerificationStateChange={(identifier, verification, checked) => { From 0a50fef379b660757018e076d3589aadbf8b0fc0 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Wed, 9 Nov 2022 16:28:58 +0800 Subject: [PATCH 10/32] fix(console): preview forgot password (#2358) --- .../pages/SignInExperience/components/Preview.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/console/src/pages/SignInExperience/components/Preview.tsx b/packages/console/src/pages/SignInExperience/components/Preview.tsx index dfee63676..dc5c82303 100644 --- a/packages/console/src/pages/SignInExperience/components/Preview.tsx +++ b/packages/console/src/pages/SignInExperience/components/Preview.tsx @@ -1,7 +1,7 @@ import type { LanguageTag } from '@logto/language-kit'; import { languages as uiLanguageNameMapping } from '@logto/language-kit'; import type { ConnectorResponse, ConnectorMetadata, SignInExperience } from '@logto/schemas'; -import { AppearanceMode } from '@logto/schemas'; +import { ConnectorType, AppearanceMode } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import { format } from 'date-fns'; @@ -94,6 +94,14 @@ const Preview = ({ signInExperience, className }: Props) => { [] ); + const hasEmailConnector = allConnectors.some( + ({ type, enabled }) => enabled && type === ConnectorType.Email + ); + + const hasSmsConnector = allConnectors.some( + ({ type, enabled }) => enabled && type === ConnectorType.Sms + ); + return { signInExperience: { ...signInExperience, @@ -103,6 +111,10 @@ const Preview = ({ signInExperience, className }: Props) => { mode, platform: platform === 'desktopWeb' ? 'web' : 'mobile', isNative: platform === 'mobile', + forgotPassword: { + email: hasEmailConnector, + sms: hasSmsConnector, + }, }; }, [allConnectors, language, mode, platform, signInExperience]); From 6e4e5ffc0d7b7381182076a87fcb0c3443afd1a4 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Wed, 9 Nov 2022 16:36:11 +0800 Subject: [PATCH 11/32] fix(core): add check required profile to password routes (#2357) --- .../core/src/routes/session/password.test.ts | 20 +++++++++---------- packages/core/src/routes/session/password.ts | 5 +++-- packages/core/src/routes/session/utils.ts | 4 +++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/core/src/routes/session/password.test.ts b/packages/core/src/routes/session/password.test.ts index 212cd7645..0eaf50aae 100644 --- a/packages/core/src/routes/session/password.test.ts +++ b/packages/core/src/routes/session/password.test.ts @@ -8,29 +8,29 @@ import { createRequester } from '@/utils/test-utils'; import passwordRoutes, { registerRoute, signInRoute } from './password'; -const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' })); +const insertUser = jest.fn(async (..._args: unknown[]) => mockUser); const hasUser = jest.fn(async (username: string) => username === 'username1'); const findUserById = jest.fn(async (): Promise => mockUser); -const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' })); +const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser); const hasActiveUsers = jest.fn(async () => true); const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience); jest.mock('@/queries/user', () => ({ findUserById: async () => findUserById(), - findUserByIdentity: async () => ({ id: 'id', identities: {} }), - findUserByPhone: async () => ({ id: 'id' }), - findUserByEmail: async () => ({ id: 'id' }), + findUserByIdentity: async () => ({ id: mockUser.id, identities: {} }), + findUserByPhone: async () => mockUser, + findUserByEmail: async () => mockUser, updateUserById: async (...args: unknown[]) => updateUserById(...args), hasUser: async (username: string) => hasUser(username), hasUserWithIdentity: async (connectorId: string, userId: string) => - connectorId === 'connectorId' && userId === 'id', + connectorId === 'connectorId' && userId === mockUser.id, hasUserWithPhone: async (phone: string) => phone === '13000000000', hasUserWithEmail: async (email: string) => email === 'a@a.com', hasActiveUsers: async () => hasActiveUsers(), async findUserByUsername(username: string) { const roleNames = username === 'admin' ? [UserRole.Admin] : []; - return { id: 'id', username, roleNames }; + return { ...mockUser, username, roleNames }; }, })); @@ -112,7 +112,7 @@ describe('session -> password routes', () => { expect.anything(), expect.anything(), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -129,7 +129,7 @@ describe('session -> password routes', () => { expect.anything(), expect.anything(), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); @@ -146,7 +146,7 @@ describe('session -> password routes', () => { expect.anything(), expect.anything(), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ login: { accountId: 'id', ts: expect.any(Number) } }), + expect.objectContaining({ login: { accountId: mockUser.id, ts: expect.any(Number) } }), expect.anything() ); }); diff --git a/packages/core/src/routes/session/password.ts b/packages/core/src/routes/session/password.ts index 498d14f24..37a4a05ff 100644 --- a/packages/core/src/routes/session/password.ts +++ b/packages/core/src/routes/session/password.ts @@ -19,7 +19,7 @@ import { import assertThat from '@/utils/assert-that'; import type { AnonymousRouter } from '../types'; -import { getRoutePrefix, signInWithPassword } from './utils'; +import { checkRequiredProfile, getRoutePrefix, signInWithPassword } from './utils'; export const registerRoute = getRoutePrefix('register', 'password'); export const signInRoute = getRoutePrefix('sign-in', 'password'); @@ -171,7 +171,7 @@ export default function passwordRoutes(router: T, pro const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); - await insertUser({ + const user = await insertUser({ id, username, passwordEncrypted, @@ -179,6 +179,7 @@ export default function passwordRoutes(router: T, pro roleNames, lastSignInAt: Date.now(), }); + await checkRequiredProfile(ctx, provider, user, signInExperience); await assignInteractionResults(ctx, provider, { login: { accountId: id } }); return next(); diff --git a/packages/core/src/routes/session/utils.ts b/packages/core/src/routes/session/utils.ts index 0a0273f66..8ce16d05e 100644 --- a/packages/core/src/routes/session/utils.ts +++ b/packages/core/src/routes/session/utils.ts @@ -219,9 +219,11 @@ export const signInWithPassword = async ( ctx.log(logType, logPayload); const user = await findUser(); - const { id } = await verifyUserPassword(user, password); + const verifiedUser = await verifyUserPassword(user, password); + const { id } = verifiedUser; ctx.log(logType, { userId: id }); await updateUserById(id, { lastSignInAt: Date.now() }); + await checkRequiredProfile(ctx, provider, verifiedUser, signInExperience); await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true); }; From d1a5592872ff085ba5aea2ae7dc6b32151b2e065 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 9 Nov 2022 19:34:44 +0800 Subject: [PATCH 12/32] refactor(console): the swap icon should be fixed in the sign-in method item (#2361) --- packages/console/src/components/Checkbox/index.tsx | 6 ++++-- .../components/SignInMethodEditBox/SignInMethodItem.tsx | 3 +++ .../components/SignInMethodEditBox/index.module.scss | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/console/src/components/Checkbox/index.tsx b/packages/console/src/components/Checkbox/index.tsx index d71a5dc91..16a50f439 100644 --- a/packages/console/src/components/Checkbox/index.tsx +++ b/packages/console/src/components/Checkbox/index.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import { nanoid } from 'nanoid'; import type { ReactNode } from 'react'; import { useState } from 'react'; @@ -12,13 +13,14 @@ type Props = { label?: ReactNode; // eslint-disable-next-line react/boolean-prop-naming disabled: boolean; + className?: string; }; -const Checkbox = ({ value, onChange, label, disabled }: Props) => { +const Checkbox = ({ value, onChange, label, disabled, className }: Props) => { const [id, setId] = useState(nanoid()); return ( -
+
{ onToggleVerificationPrimary(identifier); @@ -71,6 +73,7 @@ const SignInMethodItem = ({ Date: Wed, 9 Nov 2022 19:41:52 +0800 Subject: [PATCH 13/32] style(console): add margins to the draggable icon in sign-in method items (#2362) --- .../components/SignInMethodEditBox/index.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss index 8edb88c20..6309679d5 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/index.module.scss @@ -52,6 +52,7 @@ .draggableIcon { color: var(--color-text-secondary); + margin-right: _.unit(1); } } From c0e9788ca9b8a64f25a317024534021b07cad852 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 9 Nov 2022 23:05:10 +0800 Subject: [PATCH 14/32] refactor(console): fix undesired appearance for popup and tooltip (#2370) --- packages/console/src/components/Dropdown/index.tsx | 1 + packages/console/src/components/ToggleTip/index.tsx | 1 + packages/console/src/components/Tooltip/index.tsx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/console/src/components/Dropdown/index.tsx b/packages/console/src/components/Dropdown/index.tsx index c10de023d..84f8770a1 100644 --- a/packages/console/src/components/Dropdown/index.tsx +++ b/packages/console/src/components/Dropdown/index.tsx @@ -54,6 +54,7 @@ const Dropdown = ({ isFullWidth && anchorRef.current ? anchorRef.current.getBoundingClientRect().width : undefined, + ...(!position && { opacity: 0 }), ...position, }, }} diff --git a/packages/console/src/components/ToggleTip/index.tsx b/packages/console/src/components/ToggleTip/index.tsx index 223633d78..1eba09dee 100644 --- a/packages/console/src/components/ToggleTip/index.tsx +++ b/packages/console/src/components/ToggleTip/index.tsx @@ -56,6 +56,7 @@ const ToggleTip = ({ isOpen={isOpen} style={{ content: { + ...(!layoutPosition && { opacity: 0 }), ...layoutPosition, }, }} diff --git a/packages/console/src/components/Tooltip/index.tsx b/packages/console/src/components/Tooltip/index.tsx index 65e57468f..3dcc7f7cb 100644 --- a/packages/console/src/components/Tooltip/index.tsx +++ b/packages/console/src/components/Tooltip/index.tsx @@ -130,7 +130,7 @@ const Tooltip = ({ From f64a0f2df4f205e1020ae54bee002e97a1cfd141 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 9 Nov 2022 23:44:55 +0800 Subject: [PATCH 15/32] feat(console): add tool tips to disabled action items in sie (#2371) --- .../src/components/Checkbox/index.module.scss | 6 ++- .../console/src/components/Checkbox/index.tsx | 14 ++++++- .../components/IconButton/index.module.scss | 38 +++++++++++------ .../src/components/IconButton/index.tsx | 42 +++++++------------ .../LanguageEditor/LanguageDetails.tsx | 4 +- .../tabs/SignUpAndSignInTab/SignUpForm.tsx | 2 + .../SignInMethodEditBox/SignInMethodItem.tsx | 13 +++++- .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ .../translation/admin-console/sign-in-exp.ts | 11 +++++ 14 files changed, 148 insertions(+), 48 deletions(-) diff --git a/packages/console/src/components/Checkbox/index.module.scss b/packages/console/src/components/Checkbox/index.module.scss index c9519086e..0de8bd811 100644 --- a/packages/console/src/components/Checkbox/index.module.scss +++ b/packages/console/src/components/Checkbox/index.module.scss @@ -20,11 +20,13 @@ } } - input { + input, + .disabledMask { position: absolute; width: 20px; height: 20px; - left: 0; + // Note: add a left value to make the input element align with the icon + left: _.unit(0.5); top: 0; margin: 0; opacity: 0%; diff --git a/packages/console/src/components/Checkbox/index.tsx b/packages/console/src/components/Checkbox/index.tsx index 16a50f439..0c341c911 100644 --- a/packages/console/src/components/Checkbox/index.tsx +++ b/packages/console/src/components/Checkbox/index.tsx @@ -1,8 +1,9 @@ import classNames from 'classnames'; import { nanoid } from 'nanoid'; import type { ReactNode } from 'react'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; +import Tooltip from '../Tooltip'; import Icon from './Icon'; import * as styles from './index.module.scss'; @@ -14,11 +15,14 @@ type Props = { // eslint-disable-next-line react/boolean-prop-naming disabled: boolean; className?: string; + disabledTooltip?: ReactNode; }; -const Checkbox = ({ value, onChange, label, disabled, className }: Props) => { +const Checkbox = ({ value, onChange, label, disabled, className, disabledTooltip }: Props) => { const [id, setId] = useState(nanoid()); + const tipRef = useRef(null); + return (
{ onChange(event.target.checked); }} /> + {disabled && disabledTooltip && ( + <> +
+ + + )} {label && }
diff --git a/packages/console/src/components/IconButton/index.module.scss b/packages/console/src/components/IconButton/index.module.scss index f77264270..390bec69e 100644 --- a/packages/console/src/components/IconButton/index.module.scss +++ b/packages/console/src/components/IconButton/index.module.scss @@ -14,13 +14,19 @@ justify-content: center; align-items: center; - > svg { - color: var(--color-text-secondary); + .icon { + > svg { + display: block; + color: var(--color-text-secondary); + } } + &:disabled { - > svg { - color: var(--color-neutral-80); + .icon { + > svg { + color: var(--color-neutral-80); + } } } @@ -40,9 +46,11 @@ height: 24px; width: 24px; - > svg { - height: 16px; - width: 16px; + .icon { + > svg { + height: 16px; + width: 16px; + } } } @@ -50,9 +58,11 @@ height: 28px; width: 28px; - > svg { - height: 20px; - width: 20px; + .icon { + > svg { + height: 20px; + width: 20px; + } } } @@ -60,9 +70,11 @@ height: 32px; width: 32px; - > svg { - height: 24px; - width: 24px; + .icon { + > svg { + height: 24px; + width: 24px; + } } } } diff --git a/packages/console/src/components/IconButton/index.tsx b/packages/console/src/components/IconButton/index.tsx index 3470d3ccc..9d3cd4b04 100644 --- a/packages/console/src/components/IconButton/index.tsx +++ b/packages/console/src/components/IconButton/index.tsx @@ -1,49 +1,35 @@ -import type { AdminConsoleKey } from '@logto/phrases'; -import type { Nullable } from '@silverhand/essentials'; import classNames from 'classnames'; -import type { ForwardedRef, HTMLProps } from 'react'; -import { forwardRef, useImperativeHandle, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import type { ForwardedRef, HTMLProps, ReactNode } from 'react'; +import { forwardRef, useRef } from 'react'; import Tooltip from '../Tooltip'; import * as styles from './index.module.scss'; export type Props = Omit, 'size' | 'type'> & { size?: 'small' | 'medium' | 'large'; - tooltip?: AdminConsoleKey; + tooltip?: ReactNode; }; const IconButton = ( { size = 'medium', children, className, tooltip, ...rest }: Props, reference: ForwardedRef ) => { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const innerReference = useRef(null); - - useImperativeHandle, Nullable>( - reference, - () => innerReference.current - ); + const tipRef = useRef(null); return ( - <> - +
{tooltip && ( - + )} - + ); }; diff --git a/packages/console/src/pages/SignInExperience/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx b/packages/console/src/pages/SignInExperience/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx index 6f28dec1e..31835157d 100644 --- a/packages/console/src/pages/SignInExperience/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx +++ b/packages/console/src/pages/SignInExperience/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx @@ -160,7 +160,7 @@ const LanguageDetails = () => {
{!isBuiltIn && ( { setIsDeletionAlertOpen(true); }} @@ -189,7 +189,7 @@ const LanguageDetails = () => { { for (const [key, value] of Object.entries( flattenTranslation(emptyUiTranslation) diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx index 4ba3be80c..0f7b7ebaa 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx @@ -100,6 +100,7 @@ const SignUpForm = () => { label={t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')} disabled={signUpIdentifier === SignUpIdentifier.Username} value={value ?? false} + disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.set_a_password')} onChange={onChange} /> )} @@ -113,6 +114,7 @@ const SignUpForm = () => { label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')} value={value ?? false} disabled={requiredVerifySignUpIdentifiers.includes(signUpIdentifier)} + disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verify_at_sign_up')} onChange={onChange} /> )} diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/SignInMethodItem.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/SignInMethodItem.tsx index 94be29e11..34c73c463 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/SignInMethodItem.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/SignInMethodEditBox/SignInMethodItem.tsx @@ -1,4 +1,5 @@ import { SignInIdentifier } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { snakeCase } from 'snake-case'; @@ -57,6 +58,7 @@ const SignInMethodItem = ({ label={t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')} value={password} disabled={!isPasswordCheckable} + disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.password_auth')} onChange={(checked) => { onVerificationStateChange(identifier, 'password', checked); }} @@ -65,7 +67,7 @@ const SignInMethodItem = ({ <> { onToggleVerificationPrimary(identifier); }} @@ -77,6 +79,7 @@ const SignInMethodItem = ({ label={t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')} value={verificationCode} disabled={!isVerificationCodeCheckable} + disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verification_code_auth')} onChange={(checked) => { onVerificationStateChange(identifier, 'verificationCode', checked); }} @@ -87,6 +90,14 @@ const SignInMethodItem = ({
{ onDelete(identifier); }} diff --git a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts index a5cca7104..5a8255e93 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts @@ -55,6 +55,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, color: { title: 'FARBE', diff --git a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts index 4dd93d3d2..b72838edc 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts @@ -77,6 +77,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', + }, }, sign_in_methods: { title: 'SIGN-IN METHODS', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts index e647d1df9..8ec6cfeed 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts @@ -79,6 +79,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, sign_in_methods: { title: 'METHODES DE CONNEXION', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts index 5f3f60b67..398d807bc 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts @@ -74,6 +74,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, sign_in_methods: { title: '로그인 방법', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts index d78fdd257..4adc76660 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts @@ -77,6 +77,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, sign_in_methods: { title: 'MÉTODOS DE LOGIN', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts index e604fbb60..2ad0381bd 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts @@ -78,6 +78,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, sign_in_methods: { title: 'OTURUM AÇMA YÖNTEMLERİ', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts index b314a6dfb..c219cf419 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts @@ -75,6 +75,17 @@ const sign_in_exp = { go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED }, }, + tip: { + set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED + verify_at_sign_up: + 'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED + password_auth: + 'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED + verification_code_auth: + 'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED + delete_sign_in_method: + 'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED + }, }, sign_in_methods: { title: '登录方式', From f54e643d72edcc243148712c6afc8302a4d61e27 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 10 Nov 2022 10:07:37 +0800 Subject: [PATCH 16/32] fix(ui): update content update content --- .../src/containers/CreateAccount/index.test.tsx | 16 ++++++++-------- .../ui/src/containers/CreateAccount/index.tsx | 2 +- .../containers/UsernameRegister/index.test.tsx | 10 +++++----- .../ui/src/containers/UsernameRegister/index.tsx | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/containers/CreateAccount/index.test.tsx b/packages/ui/src/containers/CreateAccount/index.test.tsx index 8defc0c82..27f0264c2 100644 --- a/packages/ui/src/containers/CreateAccount/index.test.tsx +++ b/packages/ui/src/containers/CreateAccount/index.test.tsx @@ -16,7 +16,7 @@ describe('', () => { expect(container.querySelector('input[name="new-username"]')).not.toBeNull(); expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); - expect(queryByText('action.create')).not.toBeNull(); + expect(queryByText('action.create_account')).not.toBeNull(); }); test('render with terms settings enabled', () => { @@ -30,7 +30,7 @@ describe('', () => { test('username and password are required', () => { const { queryByText, getByText } = renderWithPageContext(); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); fireEvent.click(submitButton); expect(queryByText('username_required')).not.toBeNull(); @@ -41,7 +41,7 @@ describe('', () => { test('username with initial numeric char should throw', () => { const { queryByText, getByText, container } = renderWithPageContext(); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); const usernameInput = container.querySelector('input[name="new-username"]'); @@ -65,7 +65,7 @@ describe('', () => { test('username with special character should throw', () => { const { queryByText, getByText, container } = renderWithPageContext(); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); const usernameInput = container.querySelector('input[name="new-username"]'); if (usernameInput) { @@ -88,7 +88,7 @@ describe('', () => { test('password less than 6 chars should throw', () => { const { queryByText, getByText, container } = renderWithPageContext(); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); const passwordInput = container.querySelector('input[name="new-password"]'); if (passwordInput) { @@ -111,7 +111,7 @@ describe('', () => { test('password mismatch with confirmPassword should throw', () => { const { queryByText, getByText, container } = renderWithPageContext(); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); const passwordInput = container.querySelector('input[name="new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const usernameInput = container.querySelector('input[name="username"]'); @@ -148,7 +148,7 @@ describe('', () => { const passwordInput = container.querySelector('input[name="new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const usernameInput = container.querySelector('input[name="new-username"]'); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); if (usernameInput) { fireEvent.change(usernameInput, { target: { value: 'username' } }); @@ -198,7 +198,7 @@ describe('', () => { ); - const submitButton = getByText('action.create'); + const submitButton = getByText('action.create_account'); const passwordInput = container.querySelector('input[name="new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const usernameInput = container.querySelector('input[name="new-username"]'); diff --git a/packages/ui/src/containers/CreateAccount/index.tsx b/packages/ui/src/containers/CreateAccount/index.tsx index ccb7751fc..241f84e68 100644 --- a/packages/ui/src/containers/CreateAccount/index.tsx +++ b/packages/ui/src/containers/CreateAccount/index.tsx @@ -124,7 +124,7 @@ const CreateAccount = ({ className, autoFocus }: Props) => { -
) : ( { clearErrorMessage(); @@ -67,7 +68,7 @@ const PasscodeValidation = ({ type, method, className, hasPasswordButton, target /> )} {type === UserFlow.signIn && hasPasswordButton && ( - + )} ); From c03844f12e018505e4c0df3d185c36384dcef2d3 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 10 Nov 2022 16:37:12 +0800 Subject: [PATCH 32/32] chore: fix tests and add changeset --- packages/core/src/queries/user.test.ts | 4 ++-- packages/core/src/routes/admin-user.test.ts | 20 ++++--------------- packages/core/src/routes/admin-user.ts | 2 +- .../tests/api/dashboard.test.ts | 2 +- .../tests/api/session.test.ts | 14 +++++++++++++ 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts index 6eb556874..c2d925c9a 100644 --- a/packages/core/src/queries/user.test.ts +++ b/packages/core/src/queries/user.test.ts @@ -67,7 +67,7 @@ describe('user query', () => { const expectSql = sql` select ${sql.join(Object.values(fields), sql`,`)} from ${table} - where ${fields.primaryEmail}=$1 + where lower(${fields.primaryEmail})=lower($1) `; mockQuery.mockImplementationOnce(async (sql, values) => { @@ -179,7 +179,7 @@ describe('user query', () => { SELECT EXISTS( select ${fields.primaryEmail} from ${table} - where ${fields.primaryEmail}=$1 + where lower(${fields.primaryEmail})=lower($1) ) `; diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index c155fc7e9..28667237c 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -117,8 +117,11 @@ describe('adminUserRoutes', () => { const username = 'MJAtLogto'; const password = 'PASSWORD'; const name = 'Michael'; + const primaryEmail = 'foo@logto.io'; - const response = await userRequest.post('/users').send({ username, password, name }); + const response = await userRequest + .post('/users') + .send({ primaryEmail, username, password, name }); expect(response.status).toEqual(200); expect(response.body).toEqual({ ...mockUserResponse, @@ -133,21 +136,6 @@ describe('adminUserRoutes', () => { const password = 'PASSWORD'; const name = 'Michael'; - // Missing input - await expect(userRequest.post('/users').send({})).resolves.toHaveProperty('status', 400); - await expect(userRequest.post('/users').send({ username, password })).resolves.toHaveProperty( - 'status', - 400 - ); - await expect(userRequest.post('/users').send({ username, name })).resolves.toHaveProperty( - 'status', - 400 - ); - await expect(userRequest.post('/users').send({ password, name })).resolves.toHaveProperty( - 'status', - 400 - ); - // Invalid input format await expect( userRequest.post('/users').send({ username, password: 'abc', name }) diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index 868da92c8..5fda0a90b 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -125,7 +125,7 @@ export default function adminUserRoutes(router: T) { primaryEmail: string().regex(emailRegEx).optional(), username: string().regex(usernameRegEx).optional(), password: string().regex(passwordRegEx), - name: string(), + name: string().optional(), }), }), async (ctx, next) => { diff --git a/packages/integration-tests/tests/api/dashboard.test.ts b/packages/integration-tests/tests/api/dashboard.test.ts index cb93b330b..e0c324308 100644 --- a/packages/integration-tests/tests/api/dashboard.test.ts +++ b/packages/integration-tests/tests/api/dashboard.test.ts @@ -44,7 +44,7 @@ describe('admin console dashboard', () => { const username = generateUsername(); await createUserByAdmin(username, password); - await signIn(username, password); + await signIn({ username, password }); const newActiveUserStatistics = await getActiveUsersData(); diff --git a/packages/integration-tests/tests/api/session.test.ts b/packages/integration-tests/tests/api/session.test.ts index 2c9540a43..4e0c0cfee 100644 --- a/packages/integration-tests/tests/api/session.test.ts +++ b/packages/integration-tests/tests/api/session.test.ts @@ -37,6 +37,18 @@ describe('username and password flow', () => { const username = generateUsername(); const password = generatePassword(); + beforeAll(async () => { + await setSignUpIdentifier(SignUpIdentifier.Username, true); + await setSignInMethod([ + { + identifier: SignInIdentifier.Username, + password: true, + verificationCode: false, + isPasswordPrimary: false, + }, + ]); + }); + it('register and sign in with username & password', async () => { await expect(registerNewUser(username, password)).resolves.not.toThrow(); await expect(signIn({ username, password })).resolves.not.toThrow(); @@ -51,6 +63,8 @@ describe('email and password flow', () => { assert(localPart && domain, new Error('Email address local part or domain is empty')); beforeAll(async () => { + await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig); + await setSignUpIdentifier(SignUpIdentifier.Email, true); await setSignInMethod([ { identifier: SignInIdentifier.Email,