chore: merge branch 'master' into feature/manage-language
57
.github/workflows/deploy-dev.yml
vendored
|
@ -1,57 +0,0 @@
|
|||
name: Deploy Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
concurrency: deploy-dev
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment: dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3
|
||||
|
||||
- name: Build
|
||||
run: pnpm -- lerna run build --stream
|
||||
|
||||
- name: Add official connectors
|
||||
run: pnpm add-official-connectors
|
||||
working-directory: packages/core
|
||||
|
||||
# See warning in https://pnpm.io/cli/prune
|
||||
- name: Prune
|
||||
run: rm -rf node_modules packages/*/node_modules && pnpm i
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Setup env
|
||||
working-directory: packages/core
|
||||
run: echo "$DEV_CORE_ENV" >> .env
|
||||
env:
|
||||
DEV_CORE_ENV: ${{ secrets.DEV_CORE_ENV }}
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.DEV_SSH_KEY }}
|
||||
known_hosts: ${{ secrets.DEV_SSH_KNOWN_HOSTS }}
|
||||
config: ${{ secrets.DEV_SSH_CONFIG }}
|
||||
|
||||
- name: Rsync folder
|
||||
run: rsync --filter='exclude .git' -r -a --delete-before --ignore-times ./ $DEV_SERVER_IP:~/logto
|
||||
env:
|
||||
DEV_SERVER_IP: ${{ secrets.DEV_SERVER_IP }}
|
||||
|
||||
- name: Sleep for 5 seconds
|
||||
run: sleep 5s
|
||||
|
||||
- name: Health check
|
||||
run: curl $DEV_SERVER_URL/api/status -If
|
||||
env:
|
||||
DEV_SERVER_URL: ${{ secrets.DEV_SERVER_URL }}
|
12
.github/workflows/integration-test.yml
vendored
|
@ -83,13 +83,19 @@ jobs:
|
|||
- name: Extract
|
||||
run: tar -xzf logto.tar.gz
|
||||
|
||||
- name: Run Logto
|
||||
run: node . --from-root --all-yes &
|
||||
- name: Seed database
|
||||
working-directory: logto/packages/core
|
||||
run: npm run cli db seed
|
||||
env:
|
||||
DB_URL: postgres://postgres:postgres@localhost:5432
|
||||
|
||||
- name: Run Logto
|
||||
working-directory: logto/packages/core
|
||||
run: node . --from-root --all-yes &
|
||||
env:
|
||||
INTEGRATION_TEST: true
|
||||
NODE_ENV: production
|
||||
DB_URL_DEFAULT: postgres://postgres:postgres@localhost:5432
|
||||
DB_URL: postgres://postgres:postgres@localhost:5432
|
||||
|
||||
- name: Sleep for 5 seconds
|
||||
run: sleep 5
|
||||
|
|
20
.github/workflows/release.yml
vendored
|
@ -58,6 +58,26 @@ jobs:
|
|||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
deploy-dev:
|
||||
runs-on: ubuntu-latest
|
||||
needs: dockerize
|
||||
environment: dev
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
steps:
|
||||
- name: Login via Azure CLI
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Deploy to containerapp
|
||||
uses: azure/CLI@v1
|
||||
with:
|
||||
inlineScript: |
|
||||
az config set extension.use_dynamic_install=yes_without_prompt
|
||||
az containerapp update -n logto-dev -g LogtoDev --image svhd/logto:edge
|
||||
|
||||
|
||||
create-github-release:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
|
65
CHANGELOG.md
|
@ -3,6 +3,71 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/logto/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-28)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **core:** update `koaAuth()` to inject detailed auth info (#1977)
|
||||
* **core:** update user scopes (#1922)
|
||||
|
||||
### Features
|
||||
|
||||
* **console:** auto detect language setting ([#1941](https://github.com/logto-io/logto/issues/1941)) ([49b4303](https://github.com/logto-io/logto/commit/49b430394dc961451a6abca26a95ebba8d22f68c))
|
||||
* **console:** configure M2M app access ([#1999](https://github.com/logto-io/logto/issues/1999)) ([a75f8fe](https://github.com/logto-io/logto/commit/a75f8fe959b5a0b0f670bcec83b072e4d41c7890))
|
||||
* **core,phrases:** add check protected access function ([e405ef7](https://github.com/logto-io/logto/commit/e405ef7bb8fdbf01d52ef83b19350189e32a39b6))
|
||||
* **core,schemas:** add phrases schema and GET /custom-phrases/:languageKey route ([#1905](https://github.com/logto-io/logto/issues/1905)) ([7242aa8](https://github.com/logto-io/logto/commit/7242aa8c2bbb70c51e9b00dd5e3aff595c3c2eff))
|
||||
* **core,schemas:** migration deploy cli ([#1966](https://github.com/logto-io/logto/issues/1966)) ([7cc2f4d](https://github.com/logto-io/logto/commit/7cc2f4d14219145e562cebef41ebb3963083cc89))
|
||||
* **core,schemas:** use timestamp to version migrations ([bb4bfd3](https://github.com/logto-io/logto/commit/bb4bfd3d41fdd415f68e6e13f0d4a7e8a0093933))
|
||||
* **core:** add DELETE /custom-phrases/:languageKey route ([#1919](https://github.com/logto-io/logto/issues/1919)) ([c72be69](https://github.com/logto-io/logto/commit/c72be69bea639689721651b20fd559939f6c0ce6))
|
||||
* **core:** add GET /custom-phrases route ([#1935](https://github.com/logto-io/logto/issues/1935)) ([5fe0cf4](https://github.com/logto-io/logto/commit/5fe0cf4257a72f96fc439132c7b5b58e07352aa3))
|
||||
* **core:** add POST /session/forgot-password/{email,sms}/send-passcode ([#1963](https://github.com/logto-io/logto/issues/1963)) ([af2600d](https://github.com/logto-io/logto/commit/af2600d828bf315ce57de5813168571e7042d8de))
|
||||
* **core:** add POST /session/forgot-password/{email,sms}/verify-passcode ([#1968](https://github.com/logto-io/logto/issues/1968)) ([1ea39f3](https://github.com/logto-io/logto/commit/1ea39f346367d9f300be7281a65e689bf198a65c))
|
||||
* **core:** add POST /session/forgot-password/reset ([#1972](https://github.com/logto-io/logto/issues/1972)) ([acdc86c](https://github.com/logto-io/logto/commit/acdc86c8560d30a89eccb6b0f6892221ea1bc5e0))
|
||||
* **core:** add PUT /custom-phrases/:languageKey route ([#1907](https://github.com/logto-io/logto/issues/1907)) ([0ae13f0](https://github.com/logto-io/logto/commit/0ae13f091b69c717cc17ed4f400f456f1737fc5c))
|
||||
* **core:** add ts to interaction result ([#1917](https://github.com/logto-io/logto/issues/1917)) ([e01042c](https://github.com/logto-io/logto/commit/e01042cbcd77c486afa1ee9fc2fa5c1d2df92542))
|
||||
* **core:** cannot delete custom phrase used as default language in sign-in exp ([#1951](https://github.com/logto-io/logto/issues/1951)) ([a1aef26](https://github.com/logto-io/logto/commit/a1aef26905f624569ee47e43bb3a9c9cf05b997b))
|
||||
* **core:** check migration state before app start ([#1979](https://github.com/logto-io/logto/issues/1979)) ([bf1d281](https://github.com/logto-io/logto/commit/bf1d281905bcf91a09dd8330212b6db838d65344))
|
||||
* **core:** deploy migration in transaction mode ([#1980](https://github.com/logto-io/logto/issues/1980)) ([9a89c1a](https://github.com/logto-io/logto/commit/9a89c1a200322c678e2b0246ed324c847e734fc6))
|
||||
* **core:** machine to machine apps ([cd9c697](https://github.com/logto-io/logto/commit/cd9c6978a35d9fc3a571c7bd56c972939c49a9b5))
|
||||
* **core:** save empty string as null value in DB ([#1901](https://github.com/logto-io/logto/issues/1901)) ([ecdf06e](https://github.com/logto-io/logto/commit/ecdf06ef39a177b207dc75930e96dfcf2ae12cdc))
|
||||
* **core:** support base64 format `OIDC_PRIVATE_KEYS` config in `.env` file ([#1903](https://github.com/logto-io/logto/issues/1903)) ([5bdb675](https://github.com/logto-io/logto/commit/5bdb6755d2e1bf5b6a004859561d60f1103aec69))
|
||||
* **core:** update migration state after db init ([f904b88](https://github.com/logto-io/logto/commit/f904b88f564110c1ed00b2fa1c7b3c1e168fc106))
|
||||
* **schemas:** add logto configs table ([#1940](https://github.com/logto-io/logto/issues/1940)) ([577ca48](https://github.com/logto-io/logto/commit/577ca48c072ed511550e339f2d6d1ee25cedeeac))
|
||||
* **ui:** add forget password flow ([#1952](https://github.com/logto-io/logto/issues/1952)) ([ba787b4](https://github.com/logto-io/logto/commit/ba787b434ba4dd43064c56115eabfdba9912f98a))
|
||||
* **ui:** add forget password page ([#1943](https://github.com/logto-io/logto/issues/1943)) ([39d80d9](https://github.com/logto-io/logto/commit/39d80d991235c93346c26977541d3c7040379a13))
|
||||
* **ui:** add passwordless switch ([#1976](https://github.com/logto-io/logto/issues/1976)) ([ddb0e47](https://github.com/logto-io/logto/commit/ddb0e47950b3bd7f92af2a8a5e14b201e0a10ed7))
|
||||
* **ui:** add reset password form ([#1964](https://github.com/logto-io/logto/issues/1964)) ([f97ec56](https://github.com/logto-io/logto/commit/f97ec56fbf169538cff5f8f23ed8bb67e9483b27))
|
||||
* **ui:** add reset password page ([#1961](https://github.com/logto-io/logto/issues/1961)) ([ff81b0f](https://github.com/logto-io/logto/commit/ff81b0f83e86dd3686341d3612f3f5e8f075cba6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump react sdk and essentials toolkit to support CJK characters in idToken ([2f92b43](https://github.com/logto-io/logto/commit/2f92b438644bd330fa4b8cd3698d9129ecbae282))
|
||||
* **console:** add sandbox attribute to iframe ([#1926](https://github.com/logto-io/logto/issues/1926)) ([14cb043](https://github.com/logto-io/logto/commit/14cb0439e3b7a346e6d6e1a707cdea2e7d79df52))
|
||||
* **console:** get prefixed router basename in local dev env ([ccbe5da](https://github.com/logto-io/logto/commit/ccbe5dab2d60974e9c893925d552b5fc93542490))
|
||||
* **console:** old value does not flash back on saving form ([cdbd8d7](https://github.com/logto-io/logto/commit/cdbd8d7344ad22bfc10219f732e718f437cb0668))
|
||||
* **console:** use fallback language in preview ([#1960](https://github.com/logto-io/logto/issues/1960)) ([de4c46e](https://github.com/logto-io/logto/commit/de4c46e400bb4c3f3552a984366ec99b7032ed18))
|
||||
* **core,schemas:** move alteration types into schemas src ([#2005](https://github.com/logto-io/logto/issues/2005)) ([10c1be6](https://github.com/logto-io/logto/commit/10c1be6eb76e1cb94746aee632a421aea8d4c211))
|
||||
* **core:** filter out connector-kit ([#1987](https://github.com/logto-io/logto/issues/1987)) ([f4cf89f](https://github.com/logto-io/logto/commit/f4cf89fb8deee7472d8e9bdbcb7ae7364ced1f74))
|
||||
* **phrases:** phrases-ui typo and types ([#1948](https://github.com/logto-io/logto/issues/1948)) ([2f373db](https://github.com/logto-io/logto/commit/2f373db8e43bc243973d2171867ee6e2169d280f))
|
||||
* support capital letter "Y" in command line prompt ([416f4e8](https://github.com/logto-io/logto/commit/416f4e86e390318dbb0bdb262139ca4ec72ce5fe))
|
||||
* **ui:** align mobile input outline ([#1991](https://github.com/logto-io/logto/issues/1991)) ([c9ba198](https://github.com/logto-io/logto/commit/c9ba198b59ae52d3c5b4520a98864519d7a756f7))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat(console): auto detect language setting (#1941)" (#2004) ([ad1d1e3](https://github.com/logto-io/logto/commit/ad1d1e3b592b106b3cea4703d19bab041a9d48db)), closes [#1941](https://github.com/logto-io/logto/issues/1941) [#2004](https://github.com/logto-io/logto/issues/2004)
|
||||
* Revert "fix(console): use fallback language in preview (#1960)" (#2003) ([fa98452](https://github.com/logto-io/logto/commit/fa98452fe5c5e77964289df704a578e93cba877b)), closes [#1960](https://github.com/logto-io/logto/issues/1960) [#2003](https://github.com/logto-io/logto/issues/2003)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **core:** update `koaAuth()` to inject detailed auth info ([#1977](https://github.com/logto-io/logto/issues/1977)) ([d4fc7b3](https://github.com/logto-io/logto/commit/d4fc7b3e5f4979f8419b87393bfd1af02e9a191d))
|
||||
* **core:** update user scopes ([#1922](https://github.com/logto-io/logto/issues/1922)) ([8d22b5c](https://github.com/logto-io/logto/commit/8d22b5c468e5148a3815abf93de14644cdf68e8e))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.9](https://github.com/logto-io/logto/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-09-07)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.0-beta.9",
|
||||
"version": "1.0.0-beta.10",
|
||||
"npmClient": "pnpm",
|
||||
"useWorkspaces": true,
|
||||
"changelogPreset": "conventionalcommits"
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
"bootstrap": "lerna bootstrap",
|
||||
"prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi",
|
||||
"prepack": "lerna run --stream prepack",
|
||||
"dev": "lerna run --stream prepack -- --incremental && lerna --ignore=@logto/integration-test run --parallel dev",
|
||||
"dev": "lerna run --stream prepack -- --incremental && lerna --ignore=@logto/integration-tests run --parallel dev",
|
||||
"start": "cd packages/core && NODE_ENV=production node . --from-root",
|
||||
"cli": "cd packages/core && logto",
|
||||
"alteration": "cd packages/core && pnpm alteration",
|
||||
"ci:build": "lerna run --stream build",
|
||||
"ci:lint": "lerna run --parallel lint",
|
||||
"ci:stylelint": "lerna run --parallel stylelint",
|
||||
"ci:test": "lerna run --parallel test:coverage"
|
||||
"ci:test": "lerna run --parallel test:ci"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.0",
|
||||
|
|
1
packages/cli/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
alteration-scripts/
|
2
packages/cli/bin/logto
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env node
|
||||
require('../lib/index.js');
|
1
packages/cli/jest.config.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from '@silverhand/jest-config';
|
89
packages/cli/package.json
Normal file
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "@logto/cli",
|
||||
"version": "1.0.0-beta.10",
|
||||
"description": "Logto CLI.",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"homepage": "https://github.com/logto-io/logto#readme",
|
||||
"license": "MPL-2.0",
|
||||
"main": "lib/index.js",
|
||||
"bin": {
|
||||
"logto": "bin/logto"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/logto-io/logto.git"
|
||||
},
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rimraf lib && tsc -p tsconfig.build.json",
|
||||
"start": "node .",
|
||||
"start:dev": "ts-node --files src/index.ts",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:ci": "jest",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/logto-io/logto/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/schemas": "^1.0.0-beta.10",
|
||||
"@silverhand/essentials": "^1.2.1",
|
||||
"chalk": "^4.1.2",
|
||||
"decamelize": "^5.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"find-up": "^5.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^1.0.0",
|
||||
"inquirer": "^8.2.2",
|
||||
"nanoid": "^3.3.4",
|
||||
"ora": "^5.0.0",
|
||||
"roarr": "^7.11.0",
|
||||
"semver": "^7.3.7",
|
||||
"slonik": "^30.0.0",
|
||||
"slonik-interceptor-preset": "^1.2.10",
|
||||
"slonik-sql-tag-raw": "^1.1.4",
|
||||
"tar": "^6.1.11",
|
||||
"yargs": "^17.6.0",
|
||||
"zod": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/decompress": "^4.2.4",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/tar": "^6.1.2",
|
||||
"@types/yargs": "^17.0.13",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand",
|
||||
"rules": {
|
||||
"complexity": [
|
||||
"error",
|
||||
7
|
||||
]
|
||||
}
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc"
|
||||
}
|
40
packages/cli/src/commands/database/alteration.test.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { createMockPool } from 'slonik';
|
||||
|
||||
import * as queries from '../../queries/logto-config';
|
||||
import { QueryType } from '../../test-utilities';
|
||||
import * as functions from './alteration';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
const pool = createMockPool({
|
||||
query: async (sql, values) => {
|
||||
return mockQuery(sql, values);
|
||||
},
|
||||
});
|
||||
|
||||
const files = Object.freeze([
|
||||
{ filename: '1.0.0-1663923770-a.js', path: '/alterations/1.0.0-1663923770-a.js' },
|
||||
{ filename: '1.0.0-1663923771-b.js', path: '/alterations/1.0.0-1663923771-b.js' },
|
||||
{ filename: '1.0.0-1663923772-c.js', path: '/alterations/1.0.0-1663923772-c.js' },
|
||||
]);
|
||||
|
||||
describe('getUndeployedAlterations()', () => {
|
||||
beforeEach(() => {
|
||||
// `getAlterationFiles()` will ensure the order
|
||||
jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]);
|
||||
});
|
||||
|
||||
it('returns all files if database timestamp is 0', async () => {
|
||||
jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0);
|
||||
|
||||
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files);
|
||||
});
|
||||
|
||||
it('returns files whose timestamp is greater then database timestamp', async () => {
|
||||
jest
|
||||
.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp')
|
||||
.mockResolvedValueOnce(1_663_923_770);
|
||||
|
||||
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
||||
});
|
||||
});
|
162
packages/cli/src/commands/database/alteration.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
import path from 'path';
|
||||
|
||||
import { AlterationScript } from '@logto/schemas/lib/types/alteration';
|
||||
import { conditional, conditionalString } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import findUp, { exists } from 'find-up';
|
||||
import { copy, existsSync, remove, readdir } from 'fs-extra';
|
||||
import { DatabasePool } from 'slonik';
|
||||
import { CommandModule } from 'yargs';
|
||||
|
||||
import { createPoolFromEnv } from '../../database';
|
||||
import {
|
||||
getCurrentDatabaseAlterationTimestamp,
|
||||
updateDatabaseTimestamp,
|
||||
} from '../../queries/logto-config';
|
||||
import { getPathInModule, log } from '../../utilities';
|
||||
|
||||
const alterationFileNameRegex = /-(\d+)-?.*\.js$/;
|
||||
|
||||
const getTimestampFromFileName = (fileName: string) => {
|
||||
const match = alterationFileNameRegex.exec(fileName);
|
||||
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Can not get timestamp: ${fileName}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
||||
|
||||
const importAlterationScript = async (filePath: string): Promise<AlterationScript> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(filePath);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return module.default as AlterationScript;
|
||||
};
|
||||
|
||||
type AlterationFile = { path: string; filename: string };
|
||||
|
||||
export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
||||
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations');
|
||||
|
||||
/**
|
||||
* We copy all alteration scripts to the CLI package root directory,
|
||||
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
|
||||
* While the original `@logto/schemas` may remove them in production.
|
||||
*/
|
||||
const packageDirectory = await findUp(
|
||||
async (directory) => {
|
||||
const hasPackageJson = await exists(path.join(directory, 'package.json'));
|
||||
|
||||
return conditional(hasPackageJson && directory);
|
||||
},
|
||||
{
|
||||
// Until we migrate to ESM
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
cwd: __dirname,
|
||||
type: 'directory',
|
||||
}
|
||||
);
|
||||
|
||||
const localAlterationDirectory = path.resolve(
|
||||
// Until we migrate to ESM
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
packageDirectory ?? __dirname,
|
||||
'alteration-scripts'
|
||||
);
|
||||
|
||||
if (!existsSync(alterationDirectory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We need to copy alteration files to execute in the CLI context to make `slonik` available
|
||||
await remove(localAlterationDirectory);
|
||||
await copy(alterationDirectory, localAlterationDirectory);
|
||||
|
||||
const directory = await readdir(localAlterationDirectory);
|
||||
const files = directory.filter((file) => alterationFileNameRegex.test(file));
|
||||
|
||||
return files
|
||||
.slice()
|
||||
.sort((file1, file2) => getTimestampFromFileName(file1) - getTimestampFromFileName(file2))
|
||||
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
||||
};
|
||||
|
||||
export const getLatestAlterationTimestamp = async () => {
|
||||
const files = await getAlterationFiles();
|
||||
const lastFile = files[files.length - 1];
|
||||
|
||||
if (!lastFile) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getTimestampFromFileName(lastFile.filename);
|
||||
};
|
||||
|
||||
export const getUndeployedAlterations = async (pool: DatabasePool) => {
|
||||
const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool);
|
||||
const files = await getAlterationFiles();
|
||||
|
||||
return files.filter(({ filename }) => getTimestampFromFileName(filename) > databaseTimestamp);
|
||||
};
|
||||
|
||||
const deployAlteration = async (
|
||||
pool: DatabasePool,
|
||||
{ path: filePath, filename }: AlterationFile
|
||||
) => {
|
||||
const { up } = await importAlterationScript(filePath);
|
||||
|
||||
try {
|
||||
await pool.transaction(async (connection) => {
|
||||
await up(connection);
|
||||
await updateDatabaseTimestamp(connection, getTimestampFromFileName(filename));
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
|
||||
await pool.end();
|
||||
log.error(
|
||||
`Error ocurred during running alteration ${chalk.blue(filename)}.\n\n` +
|
||||
" This alteration didn't change anything since it was in a transaction.\n" +
|
||||
' Try to fix the error and deploy again.'
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Run alteration ${filename} succeeded`);
|
||||
};
|
||||
|
||||
const alteration: CommandModule<unknown, { action: string }> = {
|
||||
command: ['alteration <action>', 'alt', 'alter'],
|
||||
describe: 'Perform database alteration',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('action', {
|
||||
describe: 'The action to perform, now it only accepts `deploy`',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async ({ action }) => {
|
||||
if (action !== 'deploy') {
|
||||
log.error('Unsupported action');
|
||||
}
|
||||
|
||||
const pool = await createPoolFromEnv();
|
||||
const alterations = await getUndeployedAlterations(pool);
|
||||
|
||||
log.info(
|
||||
`Found ${alterations.length} alteration${conditionalString(
|
||||
alterations.length > 1 && 's'
|
||||
)} to deploy`
|
||||
);
|
||||
|
||||
// The await inside the loop is intended, alterations should run in order
|
||||
for (const alteration of alterations) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deployAlteration(pool, alteration);
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
96
packages/cli/src/commands/database/config.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { logtoConfigGuards, LogtoConfigKey, logtoConfigKeys } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import { CommandModule } from 'yargs';
|
||||
|
||||
import { createPoolFromEnv } from '../../database';
|
||||
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config';
|
||||
import { deduplicate, log } from '../../utilities';
|
||||
|
||||
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
|
||||
|
||||
type ValidateKeysFunction = {
|
||||
(keys: string[]): asserts keys is LogtoConfigKey[];
|
||||
(key: string): asserts key is LogtoConfigKey;
|
||||
};
|
||||
|
||||
const validateKeys: ValidateKeysFunction = (keys) => {
|
||||
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
|
||||
// Using `.includes()` will result a type error
|
||||
// eslint-disable-next-line unicorn/prefer-includes
|
||||
(key) => !logtoConfigKeys.some((element) => element === key)
|
||||
);
|
||||
|
||||
if (invalidKey) {
|
||||
log.error(
|
||||
`Invalid config key ${chalk.red(invalidKey)} found, expected one of ${validKeysDisplay}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getConfig: CommandModule<unknown, { key: string; keys: string[] }> = {
|
||||
command: 'get-config <key> [keys...]',
|
||||
describe: 'Get config value(s) of the given key(s) in Logto database',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('key', {
|
||||
describe: `The key to get from database, one of ${validKeysDisplay}`,
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.positional('keys', {
|
||||
describe: 'The additional keys to get from database',
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
}),
|
||||
handler: async ({ key, keys }) => {
|
||||
const queryKeys = deduplicate([key, ...keys]);
|
||||
validateKeys(queryKeys);
|
||||
|
||||
const pool = await createPoolFromEnv();
|
||||
const { rows } = await getRowsByKeys(pool, queryKeys);
|
||||
await pool.end();
|
||||
|
||||
console.log(
|
||||
queryKeys
|
||||
.map((currentKey) => {
|
||||
const value = rows.find(({ key }) => currentKey === key)?.value;
|
||||
|
||||
return (
|
||||
chalk.magenta(currentKey) +
|
||||
'=' +
|
||||
(value === undefined ? chalk.gray(value) : chalk.green(JSON.stringify(value)))
|
||||
);
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const setConfig: CommandModule<unknown, { key: string; value: string }> = {
|
||||
command: 'set-config <key> <value>',
|
||||
describe: 'Set config value of the given key in Logto database',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('key', {
|
||||
describe: `The key to get from database, one of ${validKeysDisplay}`,
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.positional('value', {
|
||||
describe: 'The value to set, should be a valid JSON string',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async ({ key, value }) => {
|
||||
validateKeys(key);
|
||||
|
||||
const guarded = logtoConfigGuards[key].parse(JSON.parse(value));
|
||||
|
||||
const pool = await createPoolFromEnv();
|
||||
await updateValueByKey(pool, key, guarded);
|
||||
await pool.end();
|
||||
|
||||
log.info(`Update ${chalk.green(key)} succeeded`);
|
||||
},
|
||||
};
|
16
packages/cli/src/commands/database/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { CommandModule } from 'yargs';
|
||||
|
||||
import { noop } from '../../utilities';
|
||||
import alteration from './alteration';
|
||||
import { getConfig, setConfig } from './config';
|
||||
import seed from './seed';
|
||||
|
||||
const database: CommandModule = {
|
||||
command: ['database', 'db'],
|
||||
describe: 'Commands for Logto database',
|
||||
builder: (yargs) =>
|
||||
yargs.command(getConfig).command(setConfig).command(seed).command(alteration).demandCommand(1),
|
||||
handler: noop,
|
||||
};
|
||||
|
||||
export default database;
|
159
packages/cli/src/commands/database/seed/index.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { readdir, readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { logtoConfigGuards, LogtoOidcConfigKey, seeds } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import { DatabasePool, DatabaseTransactionConnection, sql } from 'slonik';
|
||||
import { raw } from 'slonik-sql-tag-raw';
|
||||
import { CommandModule } from 'yargs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database';
|
||||
import {
|
||||
getRowsByKeys,
|
||||
updateDatabaseTimestamp,
|
||||
updateValueByKey,
|
||||
} from '../../../queries/logto-config';
|
||||
import { buildApplicationSecret, getPathInModule, log, oraPromise } from '../../../utilities';
|
||||
import { getLatestAlterationTimestamp } from '../alteration';
|
||||
import { oidcConfigReaders } from './oidc-config';
|
||||
|
||||
const createTables = async (connection: DatabaseTransactionConnection) => {
|
||||
const tableDirectory = getPathInModule('@logto/schemas', 'tables');
|
||||
const directoryFiles = await readdir(tableDirectory);
|
||||
const tableFiles = directoryFiles.filter((file) => file.endsWith('.sql'));
|
||||
const queries = await Promise.all(
|
||||
tableFiles.map<Promise<[string, string]>>(async (file) => [
|
||||
file,
|
||||
await readFile(path.join(tableDirectory, file), 'utf8'),
|
||||
])
|
||||
);
|
||||
|
||||
// Await in loop is intended for better error handling
|
||||
for (const [, query] of queries) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await connection.query(sql`${raw(query)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const seedTables = async (connection: DatabaseTransactionConnection) => {
|
||||
const {
|
||||
managementResource,
|
||||
defaultSignInExperience,
|
||||
createDefaultSetting,
|
||||
createDemoAppApplication,
|
||||
defaultRole,
|
||||
} = seeds;
|
||||
|
||||
await Promise.all([
|
||||
connection.query(insertInto(managementResource, 'resources')),
|
||||
connection.query(insertInto(createDefaultSetting(), 'settings')),
|
||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||
connection.query(
|
||||
insertInto(createDemoAppApplication(buildApplicationSecret()), 'applications')
|
||||
),
|
||||
connection.query(insertInto(defaultRole, 'roles')),
|
||||
updateDatabaseTimestamp(connection, await getLatestAlterationTimestamp()),
|
||||
]);
|
||||
};
|
||||
|
||||
const seedOidcConfigs = async (pool: DatabaseTransactionConnection) => {
|
||||
const configGuard = z.object({
|
||||
key: z.nativeEnum(LogtoOidcConfigKey),
|
||||
value: z.unknown(),
|
||||
});
|
||||
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
||||
// Filter out valid keys that hold a valid value
|
||||
const result = await Promise.all(
|
||||
rows.map<Promise<LogtoOidcConfigKey | undefined>>(async (row) => {
|
||||
try {
|
||||
const { key, value } = await configGuard.parseAsync(row);
|
||||
await logtoConfigGuards[key].parseAsync(value);
|
||||
|
||||
return key;
|
||||
} catch {}
|
||||
})
|
||||
);
|
||||
const existingKeys = new Set(result.filter(Boolean));
|
||||
|
||||
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
||||
const included = existingKeys.has(key);
|
||||
|
||||
if (included) {
|
||||
log.info(`Key ${chalk.green(key)} exists, skipping`);
|
||||
}
|
||||
|
||||
return !included;
|
||||
});
|
||||
|
||||
// The awaits in loop is intended since we'd like to log info in sequence
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const key of validOptions) {
|
||||
const { value, fromEnv } = await oidcConfigReaders[key]();
|
||||
|
||||
if (fromEnv) {
|
||||
log.info(`Read config ${chalk.green(key)} from env`);
|
||||
} else {
|
||||
log.info(`Generated config ${chalk.green(key)}`);
|
||||
}
|
||||
|
||||
await updateValueByKey(pool, key, value);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
log.succeed('Seed OIDC config');
|
||||
};
|
||||
|
||||
const seedChoices = Object.freeze(['all', 'oidc'] as const);
|
||||
|
||||
type SeedChoice = typeof seedChoices[number];
|
||||
|
||||
export const seedByPool = async (pool: DatabasePool, type: SeedChoice) => {
|
||||
await pool.transaction(async (connection) => {
|
||||
if (type !== 'oidc') {
|
||||
await oraPromise(createTables(connection), {
|
||||
text: 'Create tables',
|
||||
prefixText: chalk.blue('[info]'),
|
||||
});
|
||||
await oraPromise(seedTables(connection), {
|
||||
text: 'Seed data',
|
||||
prefixText: chalk.blue('[info]'),
|
||||
});
|
||||
}
|
||||
|
||||
await seedOidcConfigs(connection);
|
||||
});
|
||||
};
|
||||
|
||||
const seed: CommandModule<Record<string, unknown>, { type: string }> = {
|
||||
command: 'seed [type]',
|
||||
describe: 'Create database then seed tables and data',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('type', {
|
||||
describe: 'Optional seed type',
|
||||
type: 'string',
|
||||
choices: seedChoices,
|
||||
default: 'all',
|
||||
}),
|
||||
handler: async ({ type }) => {
|
||||
const pool = await createPoolAndDatabaseIfNeeded();
|
||||
|
||||
try {
|
||||
// Cannot avoid `as` since the official type definition of `yargs` doesn't work.
|
||||
// The value of `type` can be ensured, so it's safe to use `as` here.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await seedByPool(pool, type as SeedChoice);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
console.log();
|
||||
log.warn(
|
||||
'Error ocurred during seeding your database.\n\n' +
|
||||
' Nothing has changed since the seeding process was in a transaction.\n' +
|
||||
' Try to fix the error and seed again.'
|
||||
);
|
||||
}
|
||||
await pool.end();
|
||||
},
|
||||
};
|
||||
|
||||
export default seed;
|
90
packages/cli/src/commands/database/seed/oidc-config.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { generateKeyPair } from 'crypto';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas';
|
||||
import { getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');
|
||||
|
||||
/**
|
||||
* Each config reader will do the following things in order:
|
||||
* 1. Try to read value from env (mimic the behavior from the original core)
|
||||
* 2. Generate value if #1 doesn't work
|
||||
*/
|
||||
export const oidcConfigReaders: {
|
||||
[key in LogtoOidcConfigKey]: () => Promise<{
|
||||
value: LogtoOidcConfigType[key];
|
||||
fromEnv: boolean;
|
||||
}>;
|
||||
} = {
|
||||
/**
|
||||
* Try to read private keys with the following order:
|
||||
*
|
||||
* 1. From `process.env.OIDC_PRIVATE_KEYS`.
|
||||
* 2. Fetch path from `process.env.OIDC_PRIVATE_KEY_PATHS` then read from that path.
|
||||
*
|
||||
*
|
||||
* @returns The private keys for OIDC provider.
|
||||
* @throws An error when failed to read a private key.
|
||||
*/
|
||||
[LogtoOidcConfigKey.PrivateKeys]: async () => {
|
||||
// Direct keys in env
|
||||
const privateKeys = getEnvAsStringArray('OIDC_PRIVATE_KEYS');
|
||||
|
||||
if (privateKeys.length > 0) {
|
||||
return {
|
||||
value: privateKeys.map((key) => {
|
||||
if (isBase64FormatPrivateKey(key)) {
|
||||
return Buffer.from(key, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
return key;
|
||||
}),
|
||||
fromEnv: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Read keys from files
|
||||
const privateKeyPaths = getEnvAsStringArray('OIDC_PRIVATE_KEY_PATHS');
|
||||
|
||||
if (privateKeyPaths.length > 0) {
|
||||
return {
|
||||
value: await Promise.all(privateKeyPaths.map(async (path) => readFile(path, 'utf8'))),
|
||||
fromEnv: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 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],
|
||||
fromEnv: false,
|
||||
};
|
||||
},
|
||||
[LogtoOidcConfigKey.CookieKeys]: async () => {
|
||||
const envKey = 'OIDC_COOKIE_KEYS';
|
||||
const keys = getEnvAsStringArray(envKey);
|
||||
|
||||
return { value: keys.length > 0 ? keys : [nanoid()], fromEnv: keys.length > 0 };
|
||||
},
|
||||
[LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => {
|
||||
const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL';
|
||||
const raw = Number(getEnv(envKey));
|
||||
const value = Math.max(3, raw || 0);
|
||||
|
||||
return { value, fromEnv: raw === value };
|
||||
},
|
||||
};
|
188
packages/cli/src/commands/install.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
import { execSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import { remove, writeFile } from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import * as semver from 'semver';
|
||||
import tar from 'tar';
|
||||
import { CommandModule } from 'yargs';
|
||||
|
||||
import { createPoolAndDatabaseIfNeeded, getDatabaseUrlFromEnv } from '../database';
|
||||
import { downloadFile, log, oraPromise, safeExecSync } from '../utilities';
|
||||
import { seedByPool } from './database/seed';
|
||||
|
||||
export type InstallArgs = {
|
||||
path?: string;
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
const defaultPath = path.join(os.homedir(), 'logto');
|
||||
const pgRequired = new semver.SemVer('14.0.0');
|
||||
|
||||
const validateNodeVersion = () => {
|
||||
const required = new semver.SemVer('16.0.0');
|
||||
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 (current.major > required.major) {
|
||||
log.warn(
|
||||
`Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const inquireInstancePath = async (initialPath?: string) => {
|
||||
const { instancePath } = await inquirer.prompt<{ instancePath: string }>(
|
||||
{
|
||||
name: 'instancePath',
|
||||
message: 'Where should we create your Logto instance?',
|
||||
type: 'input',
|
||||
default: defaultPath,
|
||||
filter: (value: string) => value.trim(),
|
||||
validate: (value: string) =>
|
||||
existsSync(path.resolve(value))
|
||||
? `The path ${chalk.green(value)} already exists, please try another.`
|
||||
: true,
|
||||
},
|
||||
{ instancePath: initialPath }
|
||||
);
|
||||
|
||||
return instancePath;
|
||||
};
|
||||
|
||||
const validateDatabase = async () => {
|
||||
const { hasPostgresUrl } = await inquirer.prompt<{ hasPostgresUrl?: boolean }>({
|
||||
name: 'hasPostgresUrl',
|
||||
message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`,
|
||||
type: 'confirm',
|
||||
when: () => {
|
||||
const pgOutput = safeExecSync('postgres --version') ?? '';
|
||||
// Filter out all brackets in the output since Homebrew will append `(Homebrew)`.
|
||||
const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('('));
|
||||
const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]);
|
||||
|
||||
return !pgCurrent || pgCurrent.compare(pgRequired) < 0;
|
||||
},
|
||||
});
|
||||
|
||||
if (hasPostgresUrl === false) {
|
||||
log.error('Logto requires a Postgres instance to run.');
|
||||
}
|
||||
};
|
||||
|
||||
const downloadRelease = async () => {
|
||||
const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz');
|
||||
|
||||
log.info(`Download Logto to ${tarFilePath}`);
|
||||
await downloadFile(
|
||||
'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz',
|
||||
tarFilePath
|
||||
);
|
||||
|
||||
return tarFilePath;
|
||||
};
|
||||
|
||||
const decompress = async (toPath: string, tarPath: string) => {
|
||||
try {
|
||||
await mkdir(toPath);
|
||||
await tar.extract({ file: tarPath, cwd: toPath, strip: 1 });
|
||||
} catch (error: unknown) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const installLogto = async ({ path: pathArgument = defaultPath, silent = false }: InstallArgs) => {
|
||||
validateNodeVersion();
|
||||
|
||||
// Get instance path
|
||||
const instancePath = await inquireInstancePath(conditional(silent && pathArgument));
|
||||
|
||||
// Validate database URL
|
||||
await validateDatabase();
|
||||
|
||||
// Download and decompress
|
||||
const tarPath = await downloadRelease();
|
||||
await oraPromise(
|
||||
decompress(instancePath, tarPath),
|
||||
{
|
||||
text: `Decompress to ${instancePath}`,
|
||||
prefixText: chalk.blue('[info]'),
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
try {
|
||||
// Seed database
|
||||
const pool = await createPoolAndDatabaseIfNeeded(); // It will ask for database URL and save to config
|
||||
await seedByPool(pool, 'all');
|
||||
await pool.end();
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
|
||||
const { value } = await inquirer.prompt<{ value: boolean }>({
|
||||
name: 'value',
|
||||
type: 'confirm',
|
||||
message:
|
||||
'Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n' +
|
||||
' Would you like to continue without seed?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!value) {
|
||||
await oraPromise(remove(instancePath), {
|
||||
text: 'Clean up',
|
||||
prefixText: chalk.blue('[info]'),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.info(`You can use ${chalk.green('db seed')} command to seed when ready.`);
|
||||
}
|
||||
|
||||
// Save to dot env
|
||||
const databaseUrl = await getDatabaseUrlFromEnv();
|
||||
const dotEnvPath = path.resolve(instancePath, '.env');
|
||||
await writeFile(dotEnvPath, `DB_URL=${databaseUrl}`, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
log.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`);
|
||||
|
||||
// Finale
|
||||
const startCommand = `cd ${instancePath} && npm start`;
|
||||
log.info(
|
||||
`Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}`
|
||||
);
|
||||
};
|
||||
|
||||
const install: CommandModule<unknown, { path?: string; silent?: boolean }> = {
|
||||
command: ['init', 'i', 'install'],
|
||||
describe: 'Download and run the latest Logto release',
|
||||
builder: (yargs) =>
|
||||
yargs.options({
|
||||
path: {
|
||||
alias: 'p',
|
||||
describe: 'Path of Logto, must be a non-existing path',
|
||||
type: 'string',
|
||||
},
|
||||
silent: {
|
||||
alias: 's',
|
||||
describe: 'Entering non-interactive mode',
|
||||
type: 'boolean',
|
||||
},
|
||||
}),
|
||||
handler: async ({ path, silent }) => {
|
||||
await installLogto({ path, silent });
|
||||
},
|
||||
};
|
||||
|
||||
export default install;
|
140
packages/cli/src/database.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { SchemaLike, SchemaValue, SchemaValuePrimitive } from '@logto/schemas';
|
||||
import decamelize from 'decamelize';
|
||||
import { createPool, IdentifierSqlToken, parseDsn, sql, SqlToken, stringifyDsn } from 'slonik';
|
||||
import { createInterceptors } from 'slonik-interceptor-preset';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getCliConfig, log } from './utilities';
|
||||
|
||||
export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto';
|
||||
|
||||
export const getDatabaseUrlFromEnv = async () =>
|
||||
(await getCliConfig({
|
||||
key: 'DB_URL',
|
||||
readableKey: 'Logto database URL',
|
||||
defaultValue: defaultDatabaseUrl,
|
||||
})) ?? '';
|
||||
|
||||
export const createPoolFromEnv = async () => {
|
||||
const databaseUrl = await getDatabaseUrlFromEnv();
|
||||
|
||||
return createPool(databaseUrl, {
|
||||
interceptors: createInterceptors(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a database pool with the database URL in config.
|
||||
* If the given database does not exists, it will try to create a new database by connecting to the maintenance database `postgres`.
|
||||
*
|
||||
* @returns A new database pool with the database URL in config.
|
||||
*/
|
||||
export const createPoolAndDatabaseIfNeeded = async () => {
|
||||
try {
|
||||
return await createPoolFromEnv();
|
||||
} catch (error: unknown) {
|
||||
const result = z.object({ code: z.string() }).safeParse(error);
|
||||
|
||||
// Database does not exist, try to create one
|
||||
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
||||
if (!(result.success && result.data.code === '3D000')) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
const databaseUrl = await getDatabaseUrlFromEnv();
|
||||
const dsn = parseDsn(databaseUrl);
|
||||
// It's ok to fall back to '?' since:
|
||||
// - Database name is required to connect in the previous pool
|
||||
// - It will throw error when creating database using '?'
|
||||
const databaseName = dsn.databaseName ?? '?';
|
||||
const maintenancePool = await createPool(stringifyDsn({ ...dsn, databaseName: 'postgres' }));
|
||||
await maintenancePool.query(sql`
|
||||
create database ${sql.identifier([databaseName])}
|
||||
with
|
||||
encoding = 'UTF8'
|
||||
connection_limit = -1;
|
||||
`);
|
||||
await maintenancePool.end();
|
||||
|
||||
log.succeed(`Created database ${databaseName}`);
|
||||
|
||||
return createPoolFromEnv();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Move database utils to `core-kit`
|
||||
export type Table = { table: string; fields: Record<string, string> };
|
||||
export type FieldIdentifiers<Key extends string | number | symbol> = {
|
||||
[key in Key]: IdentifierSqlToken;
|
||||
};
|
||||
|
||||
export const convertToIdentifiers = <T extends Table>({ table, fields }: T, withPrefix = false) => {
|
||||
const fieldsIdentifiers = Object.entries<string>(fields).map<
|
||||
[keyof T['fields'], IdentifierSqlToken]
|
||||
>(([key, value]) => [key, sql.identifier(withPrefix ? [table, value] : [value])]);
|
||||
|
||||
return {
|
||||
table: sql.identifier([table]),
|
||||
// Key value inferred from the original fields directly
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
fields: Object.fromEntries(fieldsIdentifiers) as FieldIdentifiers<keyof T['fields']>,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Note `undefined` is removed from the acceptable list,
|
||||
* since you should NOT call this function if ignoring the field is the desired behavior.
|
||||
* Calling this function with `null` means an explicit `null` setting in database is expected.
|
||||
* @param key The key of value. Will treat as `timestamp` if it ends with `_at` or 'At' AND value is a number;
|
||||
* @param value The value to convert.
|
||||
* @returns A primitive that can be saved into database.
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
export const convertToPrimitiveOrSql = (
|
||||
key: string,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
value: NonNullable<SchemaValue> | null
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): NonNullable<SchemaValuePrimitive> | SqlToken | null => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (['_at', 'At'].some((value) => key.endsWith(value)) && typeof value === 'number') {
|
||||
return sql`to_timestamp(${value}::double precision / 1000)`;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(`Cannot convert ${key} to primitive`);
|
||||
};
|
||||
|
||||
export const insertInto = <T extends SchemaLike>(object: T, table: string) => {
|
||||
const keys = Object.keys(object);
|
||||
|
||||
return sql`
|
||||
insert into ${sql.identifier([table])}
|
||||
(${sql.join(
|
||||
keys.map((key) => sql.identifier([decamelize(key)])),
|
||||
sql`, `
|
||||
)})
|
||||
values (${sql.join(
|
||||
keys.map((key) => convertToPrimitiveOrSql(key, object[key] ?? null)),
|
||||
sql`, `
|
||||
)})
|
||||
`;
|
||||
};
|
10
packages/cli/src/include.d/slonik-interceptor-preset.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare module 'slonik-interceptor-preset' {
|
||||
import { Interceptor } from 'slonik';
|
||||
|
||||
export const createInterceptors: (config?: {
|
||||
benchmarkQueries: boolean;
|
||||
logQueries: boolean;
|
||||
normaliseQueries: boolean;
|
||||
transformFieldNames: boolean;
|
||||
}) => readonly Interceptor[];
|
||||
}
|
25
packages/cli/src/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import dotenv from 'dotenv';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
import database from './commands/database';
|
||||
import install from './commands/install';
|
||||
|
||||
void yargs(hideBin(process.argv))
|
||||
.option('env', {
|
||||
alias: ['e', 'env-file'],
|
||||
describe: 'The path to your `.env` file',
|
||||
type: 'string',
|
||||
})
|
||||
.middleware(({ env }) => {
|
||||
dotenv.config({ path: env });
|
||||
})
|
||||
.command(install)
|
||||
.command(database)
|
||||
.demandCommand(1)
|
||||
.showHelpOnFail(false)
|
||||
.strict()
|
||||
.parserConfiguration({
|
||||
'dot-notation': false,
|
||||
})
|
||||
.parse();
|
102
packages/cli/src/queries/logto-config.test.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { AlterationStateKey, LogtoConfigs } from '@logto/schemas';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { convertToIdentifiers } from '../database';
|
||||
import { expectSqlAssert, QueryType } from '../test-utilities';
|
||||
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
const pool = createMockPool({
|
||||
query: async (sql, values) => {
|
||||
return mockQuery(sql, values);
|
||||
},
|
||||
});
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
const timestamp = 1_663_923_776;
|
||||
|
||||
describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
||||
it('returns 0 if query failed (table not found)', async () => {
|
||||
mockQuery.mockRejectedValueOnce({ code: '42P01' });
|
||||
|
||||
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 if the row is not found', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 if the value is in bad format', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
||||
|
||||
return createMockQueryResult([{ value: 'some_value' }]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0);
|
||||
});
|
||||
|
||||
it('returns the timestamp from database', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
||||
|
||||
// @ts-expect-error createMockQueryResult doesn't support jsonb
|
||||
return createMockQueryResult([{ value: { timestamp, updatedAt: 'now' } }]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toEqual(timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDatabaseTimestamp()', () => {
|
||||
const expectSql = sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values ($1, $2::jsonb)
|
||||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`;
|
||||
const updatedAt = '2022-09-21T06:32:46.583Z';
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(updatedAt));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('sends upsert sql with timestamp and updatedAt', async () => {
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([
|
||||
AlterationStateKey.AlterationState,
|
||||
JSON.stringify({ timestamp, updatedAt }),
|
||||
]);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
await updateDatabaseTimestamp(pool, timestamp);
|
||||
});
|
||||
});
|
69
packages/cli/src/queries/logto-config.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import {
|
||||
AlterationState,
|
||||
LogtoConfig,
|
||||
logtoConfigGuards,
|
||||
LogtoConfigKey,
|
||||
LogtoConfigs,
|
||||
AlterationStateKey,
|
||||
} from '@logto/schemas';
|
||||
import { DatabasePool, DatabaseTransactionConnection, sql } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { convertToIdentifiers } from '../database';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
|
||||
export const getRowsByKeys = async (
|
||||
pool: DatabasePool | DatabaseTransactionConnection,
|
||||
keys: LogtoConfigKey[]
|
||||
) =>
|
||||
pool.query<LogtoConfig>(sql`
|
||||
select ${sql.join([fields.key, fields.value], sql`,`)} from ${table}
|
||||
where ${fields.key} in (${sql.join(keys, sql`,`)})
|
||||
`);
|
||||
|
||||
export const updateValueByKey = async <T extends LogtoConfigKey>(
|
||||
pool: DatabasePool | DatabaseTransactionConnection,
|
||||
key: T,
|
||||
value: z.infer<typeof logtoConfigGuards[T]>
|
||||
) =>
|
||||
pool.query(
|
||||
sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values (${key}, ${sql.jsonb(value)})
|
||||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`
|
||||
);
|
||||
|
||||
export const getCurrentDatabaseAlterationTimestamp = async (pool: DatabasePool) => {
|
||||
try {
|
||||
const result = await pool.maybeOne<LogtoConfig>(
|
||||
sql`select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}`
|
||||
);
|
||||
const parsed = logtoConfigGuards[AlterationStateKey.AlterationState].safeParse(result?.value);
|
||||
|
||||
return (parsed.success && parsed.data.timestamp) || 0;
|
||||
} catch (error: unknown) {
|
||||
const result = z.object({ code: z.string() }).safeParse(error);
|
||||
|
||||
// Relation does not exist, treat as 0
|
||||
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
||||
if (result.success && result.data.code === '42P01') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDatabaseTimestamp = async (
|
||||
connection: DatabaseTransactionConnection,
|
||||
timestamp: number
|
||||
) => {
|
||||
const value: AlterationState = {
|
||||
timestamp,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return updateValueByKey(connection, AlterationStateKey.AlterationState, value);
|
||||
};
|
26
packages/cli/src/test-utilities.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copied from core
|
||||
|
||||
import { QueryResult, QueryResultRow } from 'slonik';
|
||||
import { PrimitiveValueExpression } from 'slonik/dist/src/types.d';
|
||||
|
||||
export type QueryType = (
|
||||
sql: string,
|
||||
values: readonly PrimitiveValueExpression[]
|
||||
) => Promise<QueryResult<QueryResultRow>>;
|
||||
|
||||
/**
|
||||
* Slonik Query Mock Utils
|
||||
**/
|
||||
export const expectSqlAssert = (sql: string, expectSql: string) => {
|
||||
expect(
|
||||
sql
|
||||
.split('\n')
|
||||
.map((row) => row.trim())
|
||||
.filter(Boolean)
|
||||
).toEqual(
|
||||
expectSql
|
||||
.split('\n')
|
||||
.map((row) => row.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
};
|
167
packages/cli/src/utilities.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { execSync } from 'child_process';
|
||||
import { createWriteStream } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { conditionalString, Optional } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import got, { Progress } from 'got';
|
||||
import { HttpsProxyAgent } from 'hpagent';
|
||||
import inquirer from 'inquirer';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import ora from 'ora';
|
||||
|
||||
export const safeExecSync = (command: string) => {
|
||||
try {
|
||||
return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
|
||||
} catch {}
|
||||
};
|
||||
|
||||
type Log = Readonly<{
|
||||
info: typeof console.log;
|
||||
succeed: typeof console.log;
|
||||
warn: typeof console.log;
|
||||
error: (...args: Parameters<typeof console.log>) => never;
|
||||
}>;
|
||||
|
||||
export const log: Log = Object.freeze({
|
||||
info: (...args) => {
|
||||
console.log(chalk.blue('[info]'), ...args);
|
||||
},
|
||||
succeed: (...args) => {
|
||||
log.info(chalk.green('✔'), ...args);
|
||||
},
|
||||
warn: (...args) => {
|
||||
console.warn(chalk.yellow('[warn]'), ...args);
|
||||
},
|
||||
error: (...args) => {
|
||||
console.error(chalk.red('[error]'), ...args);
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadFile = async (url: string, destination: string) => {
|
||||
const { HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy } = process.env;
|
||||
const file = createWriteStream(destination);
|
||||
const proxy = HTTPS_PROXY ?? https_proxy ?? HTTP_PROXY ?? http_proxy;
|
||||
const stream = got.stream(url, {
|
||||
...(proxy && { agent: { https: new HttpsProxyAgent({ proxy }) } }),
|
||||
});
|
||||
const spinner = ora({
|
||||
text: 'Connecting',
|
||||
prefixText: chalk.blue('[info]'),
|
||||
}).start();
|
||||
|
||||
stream.pipe(file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('downloadProgress', ({ total, percent }: Progress) => {
|
||||
if (!total) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
spinner.text = `${(percent * 100).toFixed(1)}%`;
|
||||
});
|
||||
|
||||
file.on('error', (error) => {
|
||||
spinner.fail();
|
||||
reject(error.message);
|
||||
});
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
spinner.succeed();
|
||||
resolve(file);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getPathInModule = (moduleName: string, relativePath = '/') =>
|
||||
// https://stackoverflow.com/a/49455609/12514940
|
||||
path.join(
|
||||
// Until we migrate to ESM
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
path.dirname(require.resolve(`${moduleName}/package.json`)),
|
||||
relativePath
|
||||
);
|
||||
|
||||
export const oraPromise = async <T>(
|
||||
promise: PromiseLike<T>,
|
||||
options?: ora.Options,
|
||||
exitOnError = false
|
||||
) => {
|
||||
const spinner = ora(options).start();
|
||||
|
||||
try {
|
||||
const result = await promise;
|
||||
spinner.succeed();
|
||||
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
spinner.fail();
|
||||
|
||||
if (exitOnError) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const cliConfig = new Map<string, Optional<string>>();
|
||||
|
||||
export type GetCliConfig = {
|
||||
key: string;
|
||||
readableKey: string;
|
||||
comments?: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export const getCliConfig = async ({ key, readableKey, comments, defaultValue }: GetCliConfig) => {
|
||||
if (cliConfig.has(key)) {
|
||||
return cliConfig.get(key);
|
||||
}
|
||||
|
||||
const { [key]: value } = process.env;
|
||||
|
||||
if (!value) {
|
||||
const { input } = await inquirer
|
||||
.prompt<{ input?: string }>({
|
||||
type: 'input',
|
||||
name: 'input',
|
||||
message: `Enter your ${readableKey}${conditionalString(comments && ' ' + comments)}`,
|
||||
default: defaultValue,
|
||||
})
|
||||
.catch(async (error) => {
|
||||
if (error.isTtyError) {
|
||||
log.error(`No ${readableKey} (${chalk.green(key)}) configured in env`);
|
||||
}
|
||||
|
||||
// The type definition does not give us type except `any`, throw it directly will honor the original behavior.
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw error;
|
||||
});
|
||||
|
||||
cliConfig.set(key, input);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
cliConfig.set(key, value);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
// TODO: Move to `@silverhand/essentials`
|
||||
// Intended
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export const noop = () => {};
|
||||
|
||||
export const deduplicate = <T>(array: T[]) => [...new Set(array)];
|
||||
|
||||
export const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
export const buildIdGenerator = (size: number) => customAlphabet(alphabet, size);
|
||||
|
||||
export const buildApplicationSecret = buildIdGenerator(21);
|
4
packages/cli/tsconfig.build.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"include": ["src"],
|
||||
}
|
14
packages/cli/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"declaration": true,
|
||||
"module": "node16",
|
||||
"target": "es2022"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"exclude": ["**/alteration-scripts"]
|
||||
}
|
6
packages/cli/tsconfig.test.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
|
@ -3,6 +3,41 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/logto/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-28)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **core:** update user scopes (#1922)
|
||||
|
||||
### Features
|
||||
|
||||
* **console:** auto detect language setting ([#1941](https://github.com/logto-io/logto/issues/1941)) ([49b4303](https://github.com/logto-io/logto/commit/49b430394dc961451a6abca26a95ebba8d22f68c))
|
||||
* **console:** configure M2M app access ([#1999](https://github.com/logto-io/logto/issues/1999)) ([a75f8fe](https://github.com/logto-io/logto/commit/a75f8fe959b5a0b0f670bcec83b072e4d41c7890))
|
||||
* **core:** machine to machine apps ([cd9c697](https://github.com/logto-io/logto/commit/cd9c6978a35d9fc3a571c7bd56c972939c49a9b5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump react sdk and essentials toolkit to support CJK characters in idToken ([2f92b43](https://github.com/logto-io/logto/commit/2f92b438644bd330fa4b8cd3698d9129ecbae282))
|
||||
* **console:** add sandbox attribute to iframe ([#1926](https://github.com/logto-io/logto/issues/1926)) ([14cb043](https://github.com/logto-io/logto/commit/14cb0439e3b7a346e6d6e1a707cdea2e7d79df52))
|
||||
* **console:** get prefixed router basename in local dev env ([ccbe5da](https://github.com/logto-io/logto/commit/ccbe5dab2d60974e9c893925d552b5fc93542490))
|
||||
* **console:** old value does not flash back on saving form ([cdbd8d7](https://github.com/logto-io/logto/commit/cdbd8d7344ad22bfc10219f732e718f437cb0668))
|
||||
* **console:** use fallback language in preview ([#1960](https://github.com/logto-io/logto/issues/1960)) ([de4c46e](https://github.com/logto-io/logto/commit/de4c46e400bb4c3f3552a984366ec99b7032ed18))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat(console): auto detect language setting (#1941)" (#2004) ([ad1d1e3](https://github.com/logto-io/logto/commit/ad1d1e3b592b106b3cea4703d19bab041a9d48db)), closes [#1941](https://github.com/logto-io/logto/issues/1941) [#2004](https://github.com/logto-io/logto/issues/2004)
|
||||
* Revert "fix(console): use fallback language in preview (#1960)" (#2003) ([fa98452](https://github.com/logto-io/logto/commit/fa98452fe5c5e77964289df704a578e93cba877b)), closes [#1960](https://github.com/logto-io/logto/issues/1960) [#2003](https://github.com/logto-io/logto/issues/2003)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **core:** update user scopes ([#1922](https://github.com/logto-io/logto/issues/1922)) ([8d22b5c](https://github.com/logto-io/logto/commit/8d22b5c468e5148a3815abf93de14644cdf68e8e))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.9](https://github.com/logto-io/logto/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-09-07)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@logto/console",
|
||||
"version": "1.0.0-beta.9",
|
||||
"version": "1.0.0-beta.10",
|
||||
"description": "> TODO: description",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"homepage": "https://github.com/logto-io/logto#readme",
|
||||
|
@ -20,17 +20,17 @@
|
|||
"@fontsource/roboto-mono": "^4.5.7",
|
||||
"@logto/core-kit": "1.0.0-beta.16",
|
||||
"@logto/language-kit": "1.0.0-beta.16",
|
||||
"@logto/phrases": "^1.0.0-beta.9",
|
||||
"@logto/phrases-ui": "^1.0.0-beta.9",
|
||||
"@logto/phrases": "^1.0.0-beta.10",
|
||||
"@logto/phrases-ui": "^1.0.0-beta.10",
|
||||
"@logto/react": "1.0.0-beta.8",
|
||||
"@logto/schemas": "^1.0.0-beta.9",
|
||||
"@logto/schemas": "^1.0.0-beta.10",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@parcel/core": "2.7.0",
|
||||
"@parcel/transformer-mdx": "2.7.0",
|
||||
"@parcel/transformer-sass": "2.7.0",
|
||||
"@parcel/transformer-svg-react": "2.7.0",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/eslint-config-react": "1.0.0",
|
||||
"@silverhand/eslint-config-react": "1.1.0",
|
||||
"@silverhand/essentials": "^1.2.1",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@silverhand/ts-config-react": "1.0.0",
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<rect x="12" y="6" width="20" height="36" rx="4" fill="url(#paint0_linear_289_11388)"/>
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<rect x="12" y="6" width="20" height="36" rx="4" fill="url(#paint0_linear_4272_17340)"/>
|
||||
<circle cx="22" cy="38" r="1.5" stroke="#F5EEFF"/>
|
||||
<path d="M17 6H27V8C27 8.55228 26.5523 9 26 9H18C17.4477 9 17 8.55228 17 8V6Z" fill="#5938A3"/>
|
||||
<path d="M41.5 24C41.5 24.0667 41.4631 24.2311 41.3162 24.5033C41.1765 24.7621 40.9619 25.0736 40.673 25.4201C40.0959 26.1121 39.2493 26.911 38.1938 27.6685C36.0789 29.1863 33.179 30.5 30 30.5C26.821 30.5 23.9211 29.1863 21.8062 27.6685C20.7507 26.911 19.9041 26.1121 19.327 25.4201C19.0381 25.0736 18.8235 24.7621 18.6838 24.5033C18.5369 24.2311 18.5 24.0667 18.5 24C18.5 23.9333 18.5369 23.7689 18.6838 23.4967C18.8235 23.2379 19.0381 22.9264 19.327 22.5799C19.9041 21.8879 20.7507 21.089 21.8062 20.3315C23.9211 18.8137 26.821 17.5 30 17.5C33.179 17.5 36.0789 18.8137 38.1938 20.3315C39.2493 21.089 40.0959 21.8879 40.673 22.5799C40.9619 22.9264 41.1765 23.2379 41.3162 23.4967C41.4631 23.7689 41.5 23.9333 41.5 24Z" fill="#F5EEFF" stroke="#343653"/>
|
||||
<circle cx="30" cy="24" r="4" fill="#191C1D"/>
|
||||
<path d="M19 9C19 8.44772 19.4477 8 20 8H24C24.5523 8 25 8.44772 25 9C25 9.55228 24.5523 10 24 10H20C19.4477 10 19 9.55228 19 9Z" fill="#5938A3"/>
|
||||
<path opacity="0.64" d="M42 24C42 25.5327 36.6274 31 30 31C23.3726 31 18 25.5327 18 24C18 22.4673 23.3726 17 30 17C31.3242 17 32.5983 17.2183 33.7895 17.5798C34.9482 17.9315 36.0285 18.4188 37 18.9726C40.0281 20.6988 42 23.0715 42 24Z" fill="#FAABFF"/>
|
||||
<circle cx="30" cy="24" r="4" fill="#302F38"/>
|
||||
<circle cx="33" cy="24" r="2" fill="#F07EFF"/>
|
||||
<rect x="30" y="12" width="1" height="4" rx="0.5" fill="#F4E560"/>
|
||||
<rect x="36.5605" y="13.2324" width="1" height="4" rx="0.5" transform="rotate(45 36.5605 13.2324)" fill="#7072A0"/>
|
||||
<rect width="1" height="4" rx="0.5" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 26.5605 16.7681)" fill="#F4E560"/>
|
||||
<rect x="30" y="11" width="1" height="4" rx="0.5" fill="#FFFBF7"/>
|
||||
<rect x="36.5605" y="12.2324" width="1" height="4" rx="0.5" transform="rotate(45 36.5605 12.2324)" fill="#FAABFF"/>
|
||||
<rect width="1" height="4" rx="0.5" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 26.5605 15.7681)" fill="#FFFBF7"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11388" x1="10.3571" y1="26.625" x2="33.6675" y2="21.8434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_4272_17340" x1="15.4375" y1="37.125" x2="36.7316" y2="24.4696" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -1,18 +1,18 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<rect x="12" y="6" width="20" height="36" rx="4" fill="url(#paint0_linear_3406_128416)"/>
|
||||
<circle cx="22" cy="38" r="1.5" stroke="#E6DEFF"/>
|
||||
<path d="M17 6H27V8C27 8.55228 26.5523 9 26 9H18C17.4477 9 17 8.55228 17 8V6Z" fill="#4300DA"/>
|
||||
<path d="M41.5 24C41.5 24.0667 41.4631 24.2311 41.3162 24.5033C41.1765 24.7621 40.9619 25.0736 40.673 25.4201C40.0959 26.1121 39.2493 26.911 38.1938 27.6685C36.0789 29.1863 33.179 30.5 30 30.5C26.821 30.5 23.9211 29.1863 21.8062 27.6685C20.7507 26.911 19.9041 26.1121 19.327 25.4201C19.0381 25.0736 18.8235 24.7621 18.6838 24.5033C18.5369 24.2311 18.5 24.0667 18.5 24C18.5 23.9333 18.5369 23.7689 18.6838 23.4967C18.8235 23.2379 19.0381 22.9264 19.327 22.5799C19.9041 21.8879 20.7507 21.089 21.8062 20.3315C23.9211 18.8137 26.821 17.5 30 17.5C33.179 17.5 36.0789 18.8137 38.1938 20.3315C39.2493 21.089 40.0959 21.8879 40.673 22.5799C40.9619 22.9264 41.1765 23.2379 41.3162 23.4967C41.4631 23.7689 41.5 23.9333 41.5 24Z" fill="#F5EEFF" stroke="black"/>
|
||||
<circle cx="30" cy="24" r="4" fill="#191C1D"/>
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<rect x="12" y="6" width="20" height="36" rx="4" fill="url(#paint0_linear_1555_9124)"/>
|
||||
<circle cx="22" cy="38" r="1.5" stroke="#CABEFF"/>
|
||||
<path d="M19 9C19 8.44772 19.4477 8 20 8H24C24.5523 8 25 8.44772 25 9C25 9.55228 24.5523 10 24 10H20C19.4477 10 19 9.55228 19 9Z" fill="#302F38"/>
|
||||
<path opacity="0.64" d="M42 24C42 25.5327 36.6274 31 30 31C23.3726 31 18 25.5327 18 24C18 22.4673 23.3726 17 30 17C31.3242 17 32.5983 17.2183 33.7895 17.5798C34.9482 17.9315 36.0285 18.4188 37 18.9726C40.0281 20.6988 42 23.0715 42 24Z" fill="#FAABFF"/>
|
||||
<circle cx="30" cy="24" r="4" fill="#302F38"/>
|
||||
<circle cx="33" cy="24" r="2" fill="#F07EFF"/>
|
||||
<rect x="30" y="12" width="1" height="4" rx="0.5" fill="#F4E560"/>
|
||||
<rect x="36.5625" y="13.2324" width="1" height="4" rx="0.5" transform="rotate(45 36.5625 13.2324)" fill="black"/>
|
||||
<rect width="1" height="4" rx="0.5" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 26.5625 16.7676)" fill="#F4E560"/>
|
||||
<rect x="30" y="11" width="1" height="4" rx="0.5" fill="#FFFBF7"/>
|
||||
<rect x="36.5605" y="12.2324" width="1" height="4" rx="0.5" transform="rotate(45 36.5605 12.2324)" fill="#F07EFF"/>
|
||||
<rect width="1" height="4" rx="0.5" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 26.5605 15.7681)" fill="#FFFBF7"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128416" x1="10.3571" y1="26.625" x2="33.6675" y2="21.8434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_1555_9124" x1="15.4375" y1="37.125" x2="36.7316" y2="24.4696" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -1,18 +1,17 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<path d="M7 12C7 9.79086 8.79086 8 11 8L37 8C39.2091 8 41 9.79086 41 12V30C41 32.2091 39.2091 34 37 34H11C8.79086 34 7 32.2091 7 30L7 12Z" fill="url(#paint0_linear_289_11389)"/>
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<path d="M7 12C7 9.79086 8.79086 8 11 8L37 8C39.2091 8 41 9.79086 41 12V30C41 32.2091 39.2091 34 37 34H11C8.79086 34 7 32.2091 7 30L7 12Z" fill="url(#paint0_linear_4272_17341)"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" stroke="#3A3B59" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" stroke="#3A3B59" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" stroke="#3A3B59" stroke-width="0.5"/>
|
||||
<rect x="14" y="11" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="30" y="18" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="19" y="25" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="14.5" y="36.5" width="19" height="3" rx="1.5" fill="#878ABF" stroke="#555786"/>
|
||||
<rect x="14" y="11" width="4" height="6" rx="2" fill="#947DFF"/>
|
||||
<rect x="30" y="18" width="4" height="6" rx="2" fill="#947DFF"/>
|
||||
<rect x="19" y="25" width="4" height="6" rx="2" fill="#947DFF"/>
|
||||
<rect x="14" y="36" width="20" height="4" rx="2" fill="#AF9EFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11389" x1="5.11111" y1="26.5714" x2="36.9255" y2="7.71964" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="0.5" stop-color="#FC85E9"/>
|
||||
<stop offset="1" stop-color="#EC78FF"/>
|
||||
<linearGradient id="paint0_linear_4272_17341" x1="35.5104" y1="10.9792" x2="20.4222" y2="35.5618" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F07EFF"/>
|
||||
<stop offset="1" stop-color="#FFF480"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -1,18 +1,26 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<path d="M7 12C7 9.79086 8.79086 8 11 8L37 8C39.2091 8 41 9.79086 41 12V30C41 32.2091 39.2091 34 37 34H11C8.79086 34 7 32.2091 7 30L7 12Z" fill="url(#paint0_linear_3406_128353)"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF" stroke="black" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF" stroke="black" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF" stroke="black" stroke-width="0.5"/>
|
||||
<rect x="14" y="11" width="4" height="6" rx="2" fill="#5D34F2"/>
|
||||
<rect x="30" y="18" width="4" height="6" rx="2" fill="#5D34F2"/>
|
||||
<rect x="19" y="25" width="4" height="6" rx="2" fill="#5D34F2"/>
|
||||
<rect x="14.5" y="36.5" width="19" height="3" rx="1.5" fill="#F5EEFF" stroke="black"/>
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<path d="M7 12C7 9.79086 8.79086 8 11 8L37 8C39.2091 8 41 9.79086 41 12V30C41 32.2091 39.2091 34 37 34H11C8.79086 34 7 32.2091 7 30L7 12Z" fill="url(#paint0_linear_1555_9146)"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" stroke="#191C1D" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" stroke="#C4C7C7" stroke-opacity="0.02" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="14.25" width="27.5" height="0.5" rx="0.25" stroke="#CABEFF" stroke-opacity="0.14" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" stroke="#191C1D" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" stroke="#C4C7C7" stroke-opacity="0.02" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="21.25" width="27.5" height="0.5" rx="0.25" stroke="#CABEFF" stroke-opacity="0.14" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" fill="#AF9EFF"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" stroke="#191C1D" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" stroke="#C4C7C7" stroke-opacity="0.02" stroke-width="0.5"/>
|
||||
<rect x="10.25" y="28.25" width="27.5" height="0.5" rx="0.25" stroke="#CABEFF" stroke-opacity="0.14" stroke-width="0.5"/>
|
||||
<rect x="14" y="11" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="30" y="18" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="19" y="25" width="4" height="6" rx="2" fill="#7958FF"/>
|
||||
<rect x="14" y="36" width="20" height="4" rx="2" fill="#7958FF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128353" x1="5.11111" y1="26.5714" x2="36.9255" y2="7.71964" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="0.5" stop-color="#FC85E9"/>
|
||||
<stop offset="1" stop-color="#EC78FF"/>
|
||||
<linearGradient id="paint0_linear_1555_9146" x1="35.5104" y1="10.9792" x2="20.4222" y2="35.5618" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F07EFF"/>
|
||||
<stop offset="1" stop-color="#FFF480"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2 KiB |
|
@ -1,23 +1,23 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<path d="M12.1319 13.1007C11.7414 12.7101 11.7414 12.077 12.1319 11.6864C12.5224 11.2959 13.1556 11.2959 13.5461 11.6864L22.7385 20.8788L21.3243 22.293L12.1319 13.1007Z" fill="#7958FF"/>
|
||||
<path d="M8.59634 10.9793C7.81529 10.1983 7.81529 8.93196 8.59634 8.15091C9.37739 7.36986 10.6437 7.36986 11.4248 8.15091L14.2532 10.9793C15.0342 11.7604 15.0342 13.0267 14.2532 13.8078C13.4721 14.5888 12.2058 14.5888 11.4248 13.8078L8.59634 10.9793Z" fill="#7958FF"/>
|
||||
<path d="M21.3242 29.3641C18.981 27.021 18.981 23.222 21.3242 20.8788C23.6673 18.5357 27.4663 18.5357 29.8095 20.8788L38.2947 29.3641C40.6379 31.7072 40.6379 35.5062 38.2947 37.8494C35.9516 40.1925 32.1526 40.1925 29.8095 37.8494L21.3242 29.3641Z" fill="url(#paint0_linear_289_11391)"/>
|
||||
<rect x="22.0312" y="25.8286" width="2" height="16" rx="1" transform="rotate(-45 22.0312 25.8286)" fill="#CABEFF"/>
|
||||
<rect x="24.8599" y="23.0001" width="2" height="16" rx="1" transform="rotate(-45 24.8599 23.0001)" fill="#CABEFF"/>
|
||||
<path d="M23.2946 16.051C24.8567 14.4889 27.3894 14.4889 28.9515 16.051L30.3657 17.4652C31.9278 19.0273 31.9278 21.56 30.3657 23.1221L14.8093 38.6784C13.2472 40.2405 10.7146 40.2405 9.15248 38.6784L7.73827 37.2642C6.17617 35.7021 6.17617 33.1695 7.73827 31.6074L23.2946 16.051Z" fill="url(#paint1_linear_289_11391)"/>
|
||||
<rect x="21.1733" y="23.8292" width="2" height="16" rx="1" transform="rotate(45 21.1733 23.8292)" fill="#B545CA"/>
|
||||
<path d="M37.7903 22.0614C34.0803 25.7714 28.0652 25.7714 24.3552 22.0614C20.6453 18.3514 20.6453 12.3363 24.3552 8.62635C26.3753 6.60625 29.0781 5.68597 31.722 5.86593C32.2149 5.89948 32.5008 6.19053 32.5993 6.56284C32.7027 6.95322 32.5994 7.45322 32.2123 7.84039L29.305 10.7477C28.3287 11.724 28.3287 13.3069 29.305 14.2832L32.1334 17.1116C33.1097 18.0879 34.6926 18.0879 35.6689 17.1116L38.5762 14.2044C38.9634 13.8172 39.4634 13.714 39.8538 13.8173C40.2261 13.9158 40.5171 14.2017 40.5507 14.6946C40.7306 17.3385 39.8104 20.0413 37.7903 22.0614Z" fill="#E6DEFF" stroke="#2C2D44"/>
|
||||
<circle cx="7" cy="19" r="1.5" stroke="#83B983"/>
|
||||
<circle cx="42" cy="27" r="1.5" stroke="#ACA5C0"/>
|
||||
<rect x="36.879" y="7" width="3" height="3" rx="0.5" transform="rotate(-45 36.879 7)" stroke="#8B87A6"/>
|
||||
<rect x="19.879" y="40" width="3" height="3" rx="0.5" transform="rotate(-45 19.879 40)" stroke="#787A9B"/>
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<path d="M12.1309 13.1007C11.7404 12.7101 11.7404 12.077 12.1309 11.6865C12.5215 11.2959 13.1546 11.2959 13.5451 11.6865L22.7375 20.8788L21.3233 22.2931L12.1309 13.1007Z" fill="#7958FF"/>
|
||||
<path d="M8.59585 10.9794C7.81481 10.1983 7.81481 8.93197 8.59585 8.15093C9.3769 7.36988 10.6432 7.36988 11.4243 8.15093L14.2527 10.9794C15.0338 11.7604 15.0338 13.0267 14.2527 13.8078C13.4717 14.5888 12.2053 14.5888 11.4243 13.8078L8.59585 10.9794Z" fill="#7958FF"/>
|
||||
<path d="M17.082 25.1215L25.5673 16.6362L38.2952 29.3641C40.6384 31.7073 40.6384 35.5063 38.2952 37.8494C35.9521 40.1926 32.1531 40.1926 29.81 37.8494L17.082 25.1215Z" fill="url(#paint0_linear_4272_17344)"/>
|
||||
<rect x="22.0312" y="25.8286" width="2" height="16" rx="1" transform="rotate(-45 22.0312 25.8286)" fill="#AF9EFF"/>
|
||||
<rect x="24.8594" y="23.0002" width="2" height="16" rx="1" transform="rotate(-45 24.8594 23.0002)" fill="#AF9EFF"/>
|
||||
<path d="M23.2946 16.051C24.8567 14.4889 27.3894 14.4889 28.9515 16.051L30.3657 17.4652C31.9278 19.0273 31.9278 21.56 30.3657 23.1221L14.8093 38.6784C13.2472 40.2405 10.7146 40.2405 9.15248 38.6784L7.73827 37.2642C6.17617 35.7021 6.17617 33.1695 7.73827 31.6074L23.2946 16.051Z" fill="url(#paint1_linear_4272_17344)"/>
|
||||
<rect x="21.1738" y="23.8292" width="2" height="16" rx="1" transform="rotate(45 21.1738 23.8292)" fill="#B545CA"/>
|
||||
<path opacity="0.64" fill-rule="evenodd" clip-rule="evenodd" d="M24.0017 22.4149C27.9069 26.3202 34.2386 26.3202 38.1438 22.4149C40.2703 20.2885 41.2389 17.4425 41.0495 14.6607C40.9516 13.2221 39.2422 12.8313 38.2227 13.8508L35.3154 16.7581C34.5343 17.5391 33.268 17.5391 32.487 16.7581L29.6585 13.9297C28.8775 13.1486 28.8775 11.8823 29.6585 11.1012L32.5658 8.19396C33.5854 7.1744 33.1945 5.46501 31.756 5.3671C28.9741 5.17775 26.1282 6.14632 24.0017 8.27281C20.0964 12.1781 20.0964 18.5097 24.0017 22.4149Z" fill="#FAABFF"/>
|
||||
<circle cx="7" cy="19" r="1.5" stroke="#F7F8F8" stroke-opacity="0.16"/>
|
||||
<circle cx="42" cy="27" r="1.5" stroke="#F7F8F8" stroke-opacity="0.16"/>
|
||||
<rect x="36.879" y="7" width="3" height="3" rx="0.5" transform="rotate(-45 36.879 7)" stroke="#CABEFF" stroke-opacity="0.16"/>
|
||||
<rect x="19.879" y="40" width="3" height="3" rx="0.5" transform="rotate(-45 19.879 40)" stroke="#5C5F60"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11391" x1="40.3536" y1="40.8534" x2="21.8724" y2="15.8606" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_4272_17344" x1="33.2129" y1="38.3355" x2="37.6342" y2="23.7453" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_289_11391" x1="28.7933" y1="16.207" x2="3.05751" y2="23.2677" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="paint1_linear_4272_17344" x1="28.7933" y1="16.207" x2="3.05751" y2="23.2677" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="0.5" stop-color="#FC85E9"/>
|
||||
<stop offset="1" stop-color="#E264F7"/>
|
||||
|
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -1,44 +1,28 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3406_128176)">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<g opacity="0.1">
|
||||
<circle cx="24" cy="24" r="19.8" stroke="#666666" stroke-width="0.4"/>
|
||||
<circle cx="24" cy="24" r="15.8" stroke="#666666" stroke-width="0.4"/>
|
||||
<rect x="8.2" y="4.2" width="31.6" height="39.6" rx="1.8" stroke="#666666" stroke-width="0.4"/>
|
||||
<rect x="6.2" y="6.2" width="35.6" height="35.6" rx="1.8" stroke="#666666" stroke-width="0.4"/>
|
||||
<rect x="4.2" y="39.8" width="31.6" height="39.6" rx="1.8" transform="rotate(-90 4.2 39.8)" stroke="#666666" stroke-width="0.4"/>
|
||||
<rect y="44" width="4" height="4" fill="#FFF06A"/>
|
||||
<circle cx="24" cy="24" r="7.8" stroke="#666666" stroke-width="0.4"/>
|
||||
<rect x="24" width="0.2" height="48" fill="#666666"/>
|
||||
<rect x="0.101562" y="24.0996" width="0.2" height="48" transform="rotate(-90 0.101562 24.0996)" fill="#666666"/>
|
||||
</g>
|
||||
<path d="M12.129 13.1016C11.7385 12.7111 11.7385 12.0779 12.129 11.6874C12.5195 11.2969 13.1527 11.2969 13.5432 11.6874L22.7356 20.8798L21.3214 22.294L12.129 13.1016Z" fill="#7958FF"/>
|
||||
<path d="M8.5939 10.9806C7.81285 10.1996 7.81285 8.93324 8.5939 8.15219C9.37495 7.37114 10.6413 7.37114 11.4223 8.15219L14.2508 10.9806C15.0318 11.7617 15.0318 13.028 14.2508 13.809C13.4697 14.5901 12.2034 14.5901 11.4223 13.809L8.5939 10.9806Z" fill="#7958FF"/>
|
||||
<path d="M21.3208 29.3657C18.9776 27.0225 18.9776 23.2236 21.3208 20.8804C23.6639 18.5373 27.4629 18.5373 29.806 20.8804L38.2913 29.3657C40.6345 31.7088 40.6345 35.5078 38.2913 37.851C35.9482 40.1941 32.1492 40.1941 29.806 37.851L21.3208 29.3657Z" fill="url(#paint0_linear_3406_128176)"/>
|
||||
<rect x="22.0273" y="25.8301" width="2" height="16" rx="1" transform="rotate(-45 22.0273 25.8301)" fill="#CABEFF"/>
|
||||
<rect x="24.8594" y="23.002" width="2" height="16" rx="1" transform="rotate(-45 24.8594 23.002)" fill="#CABEFF"/>
|
||||
<path d="M23.2927 16.0511C24.8548 14.489 27.3874 14.489 28.9495 16.0511L30.3637 17.4653C31.9258 19.0274 31.9258 21.5601 30.3637 23.1222L14.8074 38.6785C13.2453 40.2406 10.7126 40.2406 9.15053 38.6785L7.73632 37.2643C6.17422 35.7022 6.17422 33.1695 7.73632 31.6074L23.2927 16.0511Z" fill="url(#paint1_linear_3406_128176)"/>
|
||||
<rect x="21.1719" y="23.8301" width="2" height="16" rx="1" transform="rotate(45 21.1719 23.8301)" fill="#B545CA"/>
|
||||
<path d="M37.7903 22.0623C34.0803 25.7723 28.0652 25.7723 24.3552 22.0623C20.6453 18.3523 20.6453 12.3372 24.3552 8.62726C26.3753 6.60716 29.0781 5.68689 31.722 5.86684C32.2149 5.90039 32.5008 6.19144 32.5993 6.56375C32.7027 6.95413 32.5994 7.45413 32.2123 7.84131L29.305 10.7486C28.3287 11.7249 28.3287 13.3078 29.305 14.2841L32.1334 17.1125C33.1097 18.0889 34.6926 18.0889 35.6689 17.1125L38.5762 14.2053C38.9634 13.8181 39.4634 13.7149 39.8538 13.8182C40.2261 13.9167 40.5171 14.2026 40.5507 14.6955C40.7306 17.3394 39.8104 20.0422 37.7903 22.0623Z" fill="#F5EEFF" stroke="black"/>
|
||||
<circle cx="7" cy="19" r="1.5" stroke="#9FE79F"/>
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<path d="M12.1309 13.1007C11.7404 12.7101 11.7404 12.077 12.1309 11.6864C12.5215 11.2959 13.1546 11.2959 13.5451 11.6864L22.7375 20.8788L21.3233 22.293L12.1309 13.1007Z" fill="#7958FF"/>
|
||||
<path d="M8.59585 10.9793C7.81481 10.1983 7.81481 8.93195 8.59585 8.1509C9.3769 7.36985 10.6432 7.36985 11.4243 8.1509L14.2527 10.9793C15.0338 11.7604 15.0338 13.0267 14.2527 13.8078C13.4717 14.5888 12.2053 14.5888 11.4243 13.8078L8.59585 10.9793Z" fill="#7958FF"/>
|
||||
<path d="M17.082 25.1215L25.5673 16.6362L38.2952 29.3641C40.6384 31.7072 40.6384 35.5062 38.2952 37.8494C35.9521 40.1925 32.1531 40.1925 29.81 37.8494L17.082 25.1215Z" fill="url(#paint0_linear_1555_9255)"/>
|
||||
<rect x="22.0312" y="25.8286" width="2" height="16" rx="1" transform="rotate(-45 22.0312 25.8286)" fill="#AF9EFF"/>
|
||||
<rect x="24.8594" y="23.0001" width="2" height="16" rx="1" transform="rotate(-45 24.8594 23.0001)" fill="#AF9EFF"/>
|
||||
<path d="M23.2946 16.051C24.8567 14.4889 27.3894 14.4889 28.9515 16.051L30.3657 17.4652C31.9278 19.0273 31.9278 21.56 30.3657 23.1221L14.8093 38.6784C13.2472 40.2405 10.7146 40.2405 9.15248 38.6784L7.73827 37.2642C6.17617 35.7021 6.17617 33.1695 7.73827 31.6074L23.2946 16.051Z" fill="url(#paint1_linear_1555_9255)"/>
|
||||
<rect x="21.1738" y="23.8292" width="2" height="16" rx="1" transform="rotate(45 21.1738 23.8292)" fill="#B545CA"/>
|
||||
<path opacity="0.64" fill-rule="evenodd" clip-rule="evenodd" d="M24.0017 22.4149C27.9069 26.3202 34.2386 26.3202 38.1438 22.4149C40.2703 20.2884 41.2389 17.4425 41.0495 14.6607C40.9516 13.2221 39.2422 12.8312 38.2227 13.8508L35.3154 16.7581C34.5343 17.5391 33.268 17.5391 32.487 16.7581L29.6585 13.9296C28.8775 13.1486 28.8775 11.8823 29.6585 11.1012L32.5658 8.19395C33.5854 7.17438 33.1945 5.465 31.756 5.36708C28.9741 5.17774 26.1282 6.14631 24.0017 8.27279C20.0964 12.178 20.0964 18.5097 24.0017 22.4149Z" fill="#F07EFF"/>
|
||||
<circle cx="7" cy="19" r="1.5" stroke="#FFD5FF"/>
|
||||
<circle cx="42" cy="27" r="1.5" stroke="#F7F8F8"/>
|
||||
<circle cx="42" cy="27" r="1.5" stroke="#78767F" stroke-opacity="0.02"/>
|
||||
<circle cx="42" cy="27" r="1.5" stroke="#5D34F2" stroke-opacity="0.14"/>
|
||||
<rect x="36.879" y="7" width="3" height="3" rx="0.5" transform="rotate(-45 36.879 7)" stroke="#C4C7C7"/>
|
||||
<rect x="19.879" y="40" width="3" height="3" rx="0.5" transform="rotate(-45 19.879 40)" stroke="#C4C7C7"/>
|
||||
</g>
|
||||
<rect x="36.879" y="7" width="3" height="3" rx="0.5" transform="rotate(-45 36.879 7)" stroke="#E6DEFF"/>
|
||||
<rect x="19.879" y="40" width="3" height="3" rx="0.5" transform="rotate(-45 19.879 40)" stroke="#E6DEFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128176" x1="40.3502" y1="40.855" x2="21.869" y2="15.8622" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_1555_9255" x1="33.2129" y1="38.3355" x2="37.6342" y2="23.7453" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3406_128176" x1="28.7914" y1="16.207" x2="3.05556" y2="23.2678" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="paint1_linear_1555_9255" x1="28.7933" y1="16.207" x2="3.05751" y2="23.2677" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="0.5" stop-color="#FC85E9"/>
|
||||
<stop offset="1" stop-color="#E264F7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3406_128176">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -1,21 +1,20 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<path d="M8 11C8 8.79086 9.79086 7 12 7L32 7C34.2091 7 36 8.79086 36 11V35C36 37.2091 34.2091 39 32 39H12C9.79086 39 8 37.2091 8 35L8 11Z" fill="url(#paint0_linear_289_11390)"/>
|
||||
<rect x="12" y="20" width="10" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12" y="24" width="12" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12" y="28" width="4" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12.5" y="10.5" width="7" height="7" rx="1.5" stroke="#983C8F"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 21C29 20.4477 29.4477 20 30 20H32C32.5523 20 33 20.4477 33 21V23H29V21ZM29 41C29 41.5523 29.4477 42 30 42H32C32.5523 42 33 41.5523 33 41V39H29V41ZM21 33C20.4477 33 20 32.5523 20 32V30C20 29.4477 20.4477 29 21 29H23V33H21ZM41 33C41.5523 33 42 32.5523 42 32V30C42 29.4477 41.5523 29 41 29H39V33H41Z" fill="#AC65FC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5149 25.3431C22.1244 24.9526 22.1244 24.3195 22.5149 23.9289L23.9291 22.5147C24.3197 22.1242 24.9528 22.1242 25.3433 22.5147L26.7576 23.9289L23.9291 26.7574L22.5149 25.3431ZM36.6571 39.4853C37.0476 39.8758 37.6807 39.8758 38.0713 39.4853L39.4855 38.0711C39.876 37.6805 39.876 37.0474 39.4855 36.6569L38.0713 35.2426L35.2428 38.0711L36.6571 39.4853ZM25.3433 39.4853C24.9528 39.8758 24.3197 39.8758 23.9291 39.4853L22.5149 38.0711C22.1244 37.6805 22.1244 37.0474 22.5149 36.6569L23.9291 35.2426L26.7576 38.0711L25.3433 39.4853ZM39.4855 25.3431C39.876 24.9526 39.876 24.3195 39.4855 23.9289L38.0713 22.5147C37.6807 22.1242 37.0476 22.1242 36.6571 22.5147L35.2428 23.9289L38.0713 26.7574L39.4855 25.3431Z" fill="#AC65FC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31 40C35.9706 40 40 35.9706 40 31C40 26.0294 35.9706 22 31 22C26.0294 22 22 26.0294 22 31C22 35.9706 26.0294 40 31 40ZM31 34C32.6569 34 34 32.6569 34 31C34 29.3431 32.6569 28 31 28C29.3431 28 28 29.3431 28 31C28 32.6569 29.3431 34 31 34Z" fill="url(#paint1_linear_289_11390)"/>
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<path d="M10 14C10 11.7909 11.7909 10 14 10H24L29.1849 10C30.3384 10 31.4357 10.4979 32.1952 11.366L37.0103 16.8689C37.6483 17.5981 38 18.534 38 19.5029V26V38C38 40.2091 36.2091 42 34 42H31H24H14C11.7909 42 10 40.2091 10 38L10 14Z" fill="#947DFF"/>
|
||||
<path d="M14 10C14 7.79086 15.7909 6 18 6L28 6L33.1849 6C34.3384 6 35.4357 6.49792 36.1952 7.36598L41.0103 12.8689C41.6483 13.5981 42 14.534 42 15.5029V22V34C42 36.2091 40.2091 38 38 38H18C15.7909 38 14 36.2091 14 34L14 10Z" fill="url(#paint0_linear_4272_17345)"/>
|
||||
<path d="M32 20C32 19.4477 32.4477 19 33 19H39C39.5523 19 40 19.4477 40 20C40 20.5523 39.5523 21 39 21H33C32.4477 21 32 20.5523 32 20Z" fill="#FAABFF"/>
|
||||
<path d="M32 24C32 23.4477 32.4477 23 33 23H39C39.5523 23 40 23.4477 40 24C40 24.5523 39.5523 25 39 25H33C32.4477 25 32 24.5523 32 24Z" fill="#FAABFF"/>
|
||||
<rect x="32" y="27" width="4" height="2" rx="1" fill="#FAABFF"/>
|
||||
<rect x="32" y="31" width="4" height="2" rx="1" fill="#FAABFF"/>
|
||||
<rect x="18" y="9" width="8" height="8" rx="2" fill="#FAABFF"/>
|
||||
<path opacity="0.64" d="M4 23C4 20.7909 5.79086 19 8 19H26C28.2091 19 30 20.7909 30 23V29C30 31.2091 28.2091 33 26 33H8C5.79086 33 4 31.2091 4 29V23Z" fill="#E6DEFF"/>
|
||||
<path d="M8 28.8462H10.4446C12.1954 28.8462 13.1676 27.8126 13.1676 25.9467V25.9389C13.1676 24.1637 12.1793 23.1538 10.4446 23.1538H8V28.8462ZM9.4805 27.6824V24.3176H10.1784C11.1103 24.3176 11.6589 24.9093 11.6589 25.9665V25.9744C11.6589 27.0986 11.1385 27.6824 10.1784 27.6824H9.4805Z" fill="#302F38"/>
|
||||
<path d="M16.8669 29C18.6177 29 19.7351 27.8521 19.7351 26.002V25.9941C19.7351 24.1479 18.6136 23 16.8669 23C15.1161 23 13.9946 24.1479 13.9946 25.9941V26.002C13.9946 27.8521 15.108 29 16.8669 29ZM16.8669 27.8008C16.0399 27.8008 15.4993 27.0986 15.4993 26.002V25.9941C15.4993 24.9014 16.048 24.1992 16.8669 24.1992C17.6818 24.1992 18.2264 24.9014 18.2264 25.9941V26.002C18.2264 27.0986 17.6818 27.8008 16.8669 27.8008Z" fill="#302F38"/>
|
||||
<path d="M23.3779 29C24.8705 29 25.9032 28.1124 26 26.8422V26.7791H24.576L24.5679 26.8225C24.4671 27.4103 24.0193 27.8008 23.3819 27.8008C22.567 27.8008 22.0708 27.1183 22.0708 25.998V25.9901C22.0708 24.8777 22.567 24.1992 23.3779 24.1992C24.0152 24.1992 24.4832 24.6174 24.5679 25.213L24.5719 25.2485H25.996V25.1775C25.9113 23.9191 24.8422 23 23.3779 23C21.6311 23 20.5621 24.14 20.5621 25.9941V26.002C20.5621 27.856 21.6351 29 23.3779 29Z" fill="#302F38"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11390" x1="8" y1="41.5" x2="31.504" y2="7.61547" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9CB54"/>
|
||||
<stop offset="1" stop-color="#E45CFA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_289_11390" x1="20.5214" y1="32.3125" x2="39.7606" y2="25.2088" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#D57CFF"/>
|
||||
<linearGradient id="paint0_linear_4272_17345" x1="18.8125" y1="33.6667" x2="40.3144" y2="13.5399" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
@ -1,33 +1,20 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<path d="M8 11C8 8.79086 9.79086 7 12 7L32 7C34.2091 7 36 8.79086 36 11V35C36 37.2091 34.2091 39 32 39H12C9.79086 39 8 37.2091 8 35L8 11Z" fill="url(#paint0_linear_3406_128140)"/>
|
||||
<rect x="12" y="20" width="10" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12" y="24" width="12" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12" y="28" width="4" height="2" rx="1" fill="#F4E560"/>
|
||||
<rect x="12.5" y="10.5" width="7" height="7" rx="1.5" stroke="black"/>
|
||||
<mask id="path-7-inside-1_3406_128140" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 21C29 20.4477 29.4477 20 30 20H32C32.5523 20 33 20.4477 33 21V23H29V21ZM29 41C29 41.5523 29.4477 42 30 42H32C32.5523 42 33 41.5523 33 41V39H29V41ZM21 33C20.4477 33 20 32.5523 20 32V30C20 29.4477 20.4477 29 21 29H23V33H21ZM41 33C41.5523 33 42 32.5523 42 32V30C42 29.4477 41.5523 29 41 29H39V33H41Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 21C29 20.4477 29.4477 20 30 20H32C32.5523 20 33 20.4477 33 21V23H29V21ZM29 41C29 41.5523 29.4477 42 30 42H32C32.5523 42 33 41.5523 33 41V39H29V41ZM21 33C20.4477 33 20 32.5523 20 32V30C20 29.4477 20.4477 29 21 29H23V33H21ZM41 33C41.5523 33 42 32.5523 42 32V30C42 29.4477 41.5523 29 41 29H39V33H41Z" fill="#F7F8F8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 21C29 20.4477 29.4477 20 30 20H32C32.5523 20 33 20.4477 33 21V23H29V21ZM29 41C29 41.5523 29.4477 42 30 42H32C32.5523 42 33 41.5523 33 41V39H29V41ZM21 33C20.4477 33 20 32.5523 20 32V30C20 29.4477 20.4477 29 21 29H23V33H21ZM41 33C41.5523 33 42 32.5523 42 32V30C42 29.4477 41.5523 29 41 29H39V33H41Z" fill="#78767F" fill-opacity="0.02"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 21C29 20.4477 29.4477 20 30 20H32C32.5523 20 33 20.4477 33 21V23H29V21ZM29 41C29 41.5523 29.4477 42 30 42H32C32.5523 42 33 41.5523 33 41V39H29V41ZM21 33C20.4477 33 20 32.5523 20 32V30C20 29.4477 20.4477 29 21 29H23V33H21ZM41 33C41.5523 33 42 32.5523 42 32V30C42 29.4477 41.5523 29 41 29H39V33H41Z" fill="#5D34F2" fill-opacity="0.05"/>
|
||||
<path d="M33 23V24H34V23H33ZM29 23H28V24H29V23ZM33 39H34V38H33V39ZM29 39V38H28V39H29ZM23 29H24V28H23V29ZM23 33V34H24V33H23ZM39 29V28H38V29H39ZM39 33H38V34H39V33ZM30 19C28.8954 19 28 19.8954 28 21H30V21V19ZM32 19H30V21H32V19ZM34 21C34 19.8954 33.1046 19 32 19V21H34ZM34 23V21H32V23H34ZM29 24H33V22H29V24ZM28 21V23H30V21H28ZM30 41H28C28 42.1046 28.8954 43 30 43V41ZM32 41H30V43H32V41ZM32 41V43C33.1046 43 34 42.1046 34 41H32ZM32 39V41H34V39H32ZM29 40H33V38H29V40ZM30 41V39H28V41H30ZM19 32C19 33.1046 19.8954 34 21 34V32H21H19ZM19 30V32H21V30H19ZM21 28C19.8954 28 19 28.8954 19 30H21V28ZM23 28H21V30H23V28ZM24 33V29H22V33H24ZM21 34H23V32H21V34ZM41 32V34C42.1046 34 43 33.1046 43 32H41ZM41 30V32H43V30H41ZM41 30H43C43 28.8954 42.1046 28 41 28V30ZM39 30H41V28H39V30ZM40 33V29H38V33H40ZM41 32H39V34H41V32Z" fill="black" mask="url(#path-7-inside-1_3406_128140)"/>
|
||||
<mask id="path-9-inside-2_3406_128140" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5164 25.3431C22.1259 24.9526 22.1259 24.3195 22.5164 23.9289L23.9306 22.5147C24.3211 22.1242 24.9543 22.1242 25.3448 22.5147L26.759 23.9289L23.9306 26.7574L22.5164 25.3431ZM36.6585 39.4853C37.049 39.8758 37.6822 39.8758 38.0727 39.4853L39.4869 38.0711C39.8775 37.6805 39.8775 37.0474 39.4869 36.6569L38.0727 35.2426L35.2443 38.0711L36.6585 39.4853ZM25.3448 39.4853C24.9543 39.8758 24.3211 39.8758 23.9306 39.4853L22.5164 38.0711C22.1259 37.6805 22.1259 37.0474 22.5164 36.6569L23.9306 35.2426L26.759 38.0711L25.3448 39.4853ZM39.4869 25.3431C39.8775 24.9526 39.8775 24.3195 39.4869 23.9289L38.0727 22.5147C37.6822 22.1242 37.049 22.1242 36.6585 22.5147L35.2443 23.9289L38.0727 26.7574L39.4869 25.3431Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5164 25.3431C22.1259 24.9526 22.1259 24.3195 22.5164 23.9289L23.9306 22.5147C24.3211 22.1242 24.9543 22.1242 25.3448 22.5147L26.759 23.9289L23.9306 26.7574L22.5164 25.3431ZM36.6585 39.4853C37.049 39.8758 37.6822 39.8758 38.0727 39.4853L39.4869 38.0711C39.8775 37.6805 39.8775 37.0474 39.4869 36.6569L38.0727 35.2426L35.2443 38.0711L36.6585 39.4853ZM25.3448 39.4853C24.9543 39.8758 24.3211 39.8758 23.9306 39.4853L22.5164 38.0711C22.1259 37.6805 22.1259 37.0474 22.5164 36.6569L23.9306 35.2426L26.759 38.0711L25.3448 39.4853ZM39.4869 25.3431C39.8775 24.9526 39.8775 24.3195 39.4869 23.9289L38.0727 22.5147C37.6822 22.1242 37.049 22.1242 36.6585 22.5147L35.2443 23.9289L38.0727 26.7574L39.4869 25.3431Z" fill="#F7F8F8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5164 25.3431C22.1259 24.9526 22.1259 24.3195 22.5164 23.9289L23.9306 22.5147C24.3211 22.1242 24.9543 22.1242 25.3448 22.5147L26.759 23.9289L23.9306 26.7574L22.5164 25.3431ZM36.6585 39.4853C37.049 39.8758 37.6822 39.8758 38.0727 39.4853L39.4869 38.0711C39.8775 37.6805 39.8775 37.0474 39.4869 36.6569L38.0727 35.2426L35.2443 38.0711L36.6585 39.4853ZM25.3448 39.4853C24.9543 39.8758 24.3211 39.8758 23.9306 39.4853L22.5164 38.0711C22.1259 37.6805 22.1259 37.0474 22.5164 36.6569L23.9306 35.2426L26.759 38.0711L25.3448 39.4853ZM39.4869 25.3431C39.8775 24.9526 39.8775 24.3195 39.4869 23.9289L38.0727 22.5147C37.6822 22.1242 37.049 22.1242 36.6585 22.5147L35.2443 23.9289L38.0727 26.7574L39.4869 25.3431Z" fill="#78767F" fill-opacity="0.02"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5164 25.3431C22.1259 24.9526 22.1259 24.3195 22.5164 23.9289L23.9306 22.5147C24.3211 22.1242 24.9543 22.1242 25.3448 22.5147L26.759 23.9289L23.9306 26.7574L22.5164 25.3431ZM36.6585 39.4853C37.049 39.8758 37.6822 39.8758 38.0727 39.4853L39.4869 38.0711C39.8775 37.6805 39.8775 37.0474 39.4869 36.6569L38.0727 35.2426L35.2443 38.0711L36.6585 39.4853ZM25.3448 39.4853C24.9543 39.8758 24.3211 39.8758 23.9306 39.4853L22.5164 38.0711C22.1259 37.6805 22.1259 37.0474 22.5164 36.6569L23.9306 35.2426L26.759 38.0711L25.3448 39.4853ZM39.4869 25.3431C39.8775 24.9526 39.8775 24.3195 39.4869 23.9289L38.0727 22.5147C37.6822 22.1242 37.049 22.1242 36.6585 22.5147L35.2443 23.9289L38.0727 26.7574L39.4869 25.3431Z" fill="#5D34F2" fill-opacity="0.05"/>
|
||||
<path d="M22.5164 25.3431L21.8093 26.0503L22.5164 25.3431ZM26.759 23.9289L27.4661 24.636L28.1732 23.9289L27.4661 23.2218L26.759 23.9289ZM23.9306 26.7574L23.2235 27.4645L23.9306 28.1716L24.6377 27.4645L23.9306 26.7574ZM38.0727 35.2426L38.7798 34.5355L38.0727 33.8284L37.3656 34.5355L38.0727 35.2426ZM35.2443 38.0711L34.5372 37.364L33.8301 38.0711L34.5372 38.7782L35.2443 38.0711ZM23.9306 35.2426L24.6377 34.5355L23.9306 33.8284L23.2235 34.5355L23.9306 35.2426ZM26.759 38.0711L27.4661 38.7782L28.1732 38.0711L27.4661 37.364L26.759 38.0711ZM35.2443 23.9289L34.5372 23.2218L33.8301 23.9289L34.5372 24.636L35.2443 23.9289ZM38.0727 26.7574L37.3656 27.4645L38.0727 28.1716L38.7798 27.4645L38.0727 26.7574ZM21.8093 23.2218C21.0282 24.0029 21.0282 25.2692 21.8093 26.0503L23.2235 24.636L23.2235 24.636L21.8093 23.2218ZM23.2235 21.8076L21.8093 23.2218L23.2235 24.636L24.6377 23.2218L23.2235 21.8076ZM26.0519 21.8076C25.2709 21.0266 24.0045 21.0266 23.2235 21.8076L24.6377 23.2218L26.0519 21.8076ZM27.4661 23.2218L26.0519 21.8076L24.6377 23.2218L26.0519 24.636L27.4661 23.2218ZM24.6377 27.4645L27.4661 24.636L26.0519 23.2218L23.2235 26.0503L24.6377 27.4645ZM21.8093 26.0503L23.2235 27.4645L24.6377 26.0503L23.2235 24.636L21.8093 26.0503ZM37.3656 38.7782L35.9514 40.1924C36.7325 40.9734 37.9988 40.9734 38.7798 40.1924L37.3656 38.7782ZM38.7798 37.364L37.3656 38.7782L38.7798 40.1924L40.194 38.7782L38.7798 37.364ZM38.7798 37.364L40.194 38.7782C40.9751 37.9971 40.9751 36.7308 40.194 35.9497L38.7798 37.364ZM37.3656 35.9497L38.7798 37.364L40.194 35.9497L38.7798 34.5355L37.3656 35.9497ZM35.9514 38.7782L38.7798 35.9497L37.3656 34.5355L34.5372 37.364L35.9514 38.7782ZM37.3656 38.7782L35.9514 37.364L34.5372 38.7782L35.9514 40.1924L37.3656 38.7782ZM23.2235 40.1924C24.0045 40.9734 25.2709 40.9734 26.0519 40.1924L24.6377 38.7782L24.6377 38.7782L23.2235 40.1924ZM21.8093 38.7782L23.2235 40.1924L24.6377 38.7782L23.2235 37.364L21.8093 38.7782ZM21.8093 35.9497C21.0282 36.7308 21.0282 37.9971 21.8093 38.7782L23.2235 37.364L21.8093 35.9497ZM23.2235 34.5355L21.8093 35.9497L23.2235 37.364L24.6377 35.9497L23.2235 34.5355ZM27.4661 37.364L24.6377 34.5355L23.2235 35.9497L26.0519 38.7782L27.4661 37.364ZM26.0519 40.1924L27.4661 38.7782L26.0519 37.364L24.6377 38.7782L26.0519 40.1924ZM38.7798 24.636L40.194 26.0503C40.9751 25.2692 40.9751 24.0029 40.194 23.2218L38.7798 24.636ZM37.3656 23.2218L38.7798 24.636L40.194 23.2218L38.7798 21.8076L37.3656 23.2218ZM37.3656 23.2218L38.7798 21.8076C37.9988 21.0266 36.7325 21.0266 35.9514 21.8076L37.3656 23.2218ZM35.9514 24.636L37.3656 23.2218L35.9514 21.8076L34.5372 23.2218L35.9514 24.636ZM38.7798 26.0503L35.9514 23.2218L34.5372 24.636L37.3656 27.4645L38.7798 26.0503ZM38.7798 24.636L37.3656 26.0503L38.7798 27.4645L40.194 26.0503L38.7798 24.636Z" fill="black" mask="url(#path-9-inside-2_3406_128140)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31 40C35.9706 40 40 35.9706 40 31C40 26.0294 35.9706 22 31 22C26.0294 22 22 26.0294 22 31C22 35.9706 26.0294 40 31 40ZM31 34C32.6569 34 34 32.6569 34 31C34 29.3431 32.6569 28 31 28C29.3431 28 28 29.3431 28 31C28 32.6569 29.3431 34 31 34Z" fill="url(#paint1_linear_3406_128140)"/>
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<path d="M10 14C10 11.7909 11.7909 10 14 10H24L29.1849 10C30.3384 10 31.4357 10.4979 32.1952 11.366L37.0103 16.8689C37.6483 17.5981 38 18.534 38 19.5029V26V38C38 40.2091 36.2091 42 34 42H31H24H14C11.7909 42 10 40.2091 10 38L10 14Z" fill="#CABEFF"/>
|
||||
<path d="M14 10C14 7.79086 15.7909 6 18 6L28 6L33.1849 6C34.3384 6 35.4357 6.49792 36.1952 7.36598L41.0103 12.8689C41.6483 13.5981 42 14.534 42 15.5029V22V34C42 36.2091 40.2091 38 38 38H18C15.7909 38 14 36.2091 14 34L14 10Z" fill="url(#paint0_linear_1555_9168)"/>
|
||||
<path d="M32 20C32 19.4477 32.4477 19 33 19H39C39.5523 19 40 19.4477 40 20C40 20.5523 39.5523 21 39 21H33C32.4477 21 32 20.5523 32 20Z" fill="#FAABFF"/>
|
||||
<path d="M32 24C32 23.4477 32.4477 23 33 23H39C39.5523 23 40 23.4477 40 24C40 24.5523 39.5523 25 39 25H33C32.4477 25 32 24.5523 32 24Z" fill="#FAABFF"/>
|
||||
<rect x="32" y="27" width="4" height="2" rx="1" fill="#FAABFF"/>
|
||||
<rect x="32" y="31" width="4" height="2" rx="1" fill="#FAABFF"/>
|
||||
<rect x="18" y="9" width="8" height="8" rx="2" fill="#FAABFF"/>
|
||||
<path opacity="0.64" d="M4 23C4 20.7909 5.79086 19 8 19H26C28.2091 19 30 20.7909 30 23V29C30 31.2091 28.2091 33 26 33H8C5.79086 33 4 31.2091 4 29V23Z" fill="#E6DEFF"/>
|
||||
<path d="M8 28.8462H10.4446C12.1954 28.8462 13.1676 27.8126 13.1676 25.9467V25.9389C13.1676 24.1637 12.1793 23.1538 10.4446 23.1538H8V28.8462ZM9.4805 27.6824V24.3176H10.1784C11.1103 24.3176 11.6589 24.9093 11.6589 25.9665V25.9744C11.6589 27.0986 11.1385 27.6824 10.1784 27.6824H9.4805Z" fill="#302F38"/>
|
||||
<path d="M16.8669 29C18.6177 29 19.7351 27.8521 19.7351 26.002V25.9941C19.7351 24.1479 18.6136 23 16.8669 23C15.1161 23 13.9946 24.1479 13.9946 25.9941V26.002C13.9946 27.8521 15.108 29 16.8669 29ZM16.8669 27.8008C16.0399 27.8008 15.4993 27.0986 15.4993 26.002V25.9941C15.4993 24.9014 16.048 24.1992 16.8669 24.1992C17.6818 24.1992 18.2264 24.9014 18.2264 25.9941V26.002C18.2264 27.0986 17.6818 27.8008 16.8669 27.8008Z" fill="#302F38"/>
|
||||
<path d="M23.3779 29C24.8705 29 25.9032 28.1124 26 26.8422V26.7791H24.576L24.5679 26.8225C24.4671 27.4103 24.0193 27.8008 23.3819 27.8008C22.567 27.8008 22.0708 27.1183 22.0708 25.998V25.9901C22.0708 24.8777 22.567 24.1992 23.3779 24.1992C24.0152 24.1992 24.4832 24.6174 24.5679 25.213L24.5719 25.2485H25.996V25.1775C25.9113 23.9191 24.8422 23 23.3779 23C21.6311 23 20.5621 24.14 20.5621 25.9941V26.002C20.5621 27.856 21.6351 29 23.3779 29Z" fill="#302F38"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128140" x1="8" y1="41.5" x2="31.504" y2="7.61547" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9CB54"/>
|
||||
<stop offset="1" stop-color="#E45CFA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3406_128140" x1="20.5214" y1="32.3125" x2="39.7606" y2="25.2088" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_1555_9168" x1="18.8125" y1="33.6667" x2="40.3144" y2="13.5399" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
@ -1,15 +0,0 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<path d="M25.2916 19C26.9525 17.5341 28 15.3894 28 13C28 9.64265 25.9318 6.76832 23 5.58154M14 18.2917C12.7553 16.8814 12 15.0289 12 13C12 9.64265 14.0682 6.76832 17 5.58154" stroke="#696B9E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 13C25 11.9869 24.6987 11.0441 24.1807 10.2564C23.287 8.89728 21.7483 8 20 8C17.2386 8 15 10.2386 15 13C15 13.431 15.0545 13.8492 15.1571 14.2482" stroke="#696B9E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8093 33.4583C9.06844 32.0982 8.75979 29.5844 10.1199 27.8436C11.48 26.1027 13.9937 25.7941 15.7346 27.1542L25.1907 34.5421C26.9315 35.9022 27.2402 38.416 25.8801 40.1568C24.52 41.8976 22.0062 42.2063 20.2654 40.8462L10.8093 33.4583Z" fill="#7958FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12C17.7909 12 16 13.7909 16 16V26V31V33C16 38.5228 20.4772 43 26 43H30C35.5228 43 40 38.5228 40 33V27C40 24.7909 38.2091 23 36 23C36 21.3431 34.6569 20 33 20C31.3431 20 30 21.3431 30 23V22C30 20.3431 28.6569 19 27 19C25.3431 19 24 20.3431 24 22V16C24 13.7909 22.2091 12 20 12Z" fill="url(#paint0_linear_289_11392)"/>
|
||||
<circle cx="7" cy="20" r="1.5" stroke="#CF90D3"/>
|
||||
<rect x="35.7071" y="8.82861" width="3" height="3" rx="0.5" transform="rotate(-45 35.7071 8.82861)" stroke="#83B983"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11392" x1="19.7088" y1="10.6324" x2="22.8478" y2="43.466" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DD7DFF"/>
|
||||
<stop offset="0.927083" stop-color="#694BF4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,15 +0,0 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<path d="M25.2916 19.0005C26.9525 17.5346 28 15.3899 28 13.0005C28 9.64314 25.9318 6.76881 23 5.58203M14 18.2922C12.7553 16.8819 12 15.0294 12 13.0005C12 9.64314 14.0682 6.76881 17 5.58203" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 13C25 11.9869 24.6987 11.0441 24.1807 10.2564C23.287 8.89728 21.7483 8 20 8C17.2386 8 15 10.2386 15 13C15 13.431 15.0545 13.8492 15.1571 14.2482" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8083 33.4587C9.06747 32.0987 8.75881 29.5849 10.1189 27.8441C11.479 26.1032 13.9928 25.7946 15.7336 27.1547L25.1897 34.5426C26.9305 35.9027 27.2392 38.4165 25.8791 40.1573C24.519 41.8981 22.0052 42.2068 20.2644 40.8467L10.8083 33.4587Z" fill="#7958FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12C17.7909 12 16 13.7909 16 16V26V31V33C16 38.5228 20.4772 43 26 43H30C35.5228 43 40 38.5228 40 33V27C40 24.7909 38.2091 23 36 23C36 21.3431 34.6569 20 33 20C31.3431 20 30 21.3431 30 23V22C30 20.3431 28.6569 19 27 19C25.3431 19 24 20.3431 24 22V16C24 13.7909 22.2091 12 20 12Z" fill="url(#paint0_linear_3406_128238)"/>
|
||||
<circle cx="7" cy="20" r="1.5" stroke="#FAABFF"/>
|
||||
<rect x="35.7071" y="8.82812" width="3" height="3" rx="0.5" transform="rotate(-45 35.7071 8.82812)" stroke="#9FE79F"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128238" x1="19.7088" y1="10.6324" x2="22.8478" y2="43.466" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DD7DFF"/>
|
||||
<stop offset="0.927083" stop-color="#694BF4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,21 +1,21 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_289_11393)">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#3A3B59"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 8C21 6.89543 21.8954 6 23 6H25C26.1046 6 27 6.89543 27 8V12H21V8ZM21 40C21 41.1046 21.8954 42 23 42H25C26.1046 42 27 41.1046 27 40V36H21V40ZM8 27C6.89543 27 6 26.1046 6 25V23C6 21.8954 6.89543 21 8 21H12V27H8ZM40 27C41.1046 27 42 26.1046 42 25V23C42 21.8954 41.1046 21 40 21H36V27H40Z" fill="#AF9EFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5648 14.8076C9.78371 14.0266 9.78371 12.7602 10.5648 11.9792L11.979 10.565C12.76 9.78392 14.0264 9.78392 14.8074 10.565L17.6358 13.3934L13.3932 17.636L10.5648 14.8076ZM33.1922 37.435C33.9732 38.2161 35.2396 38.2161 36.0206 37.435L37.4348 36.0208C38.2159 35.2398 38.2159 33.9734 37.4348 33.1924L34.6064 30.364L30.3637 34.6066L33.1922 37.435ZM14.8074 37.435C14.0264 38.2161 12.76 38.2161 11.979 37.435L10.5648 36.0208C9.78371 35.2398 9.78371 33.9734 10.5648 33.1924L13.3932 30.364L17.6358 34.6066L14.8074 37.435ZM37.4348 14.8076C38.2159 14.0266 38.2159 12.7602 37.4348 11.9792L36.0206 10.565C35.2396 9.78392 33.9732 9.78392 33.1922 10.565L30.3637 13.3934L34.6064 17.636L37.4348 14.8076Z" fill="#AF9EFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 38C31.732 38 38 31.732 38 24C38 16.268 31.732 10 24 10C16.268 10 10 16.268 10 24C10 31.732 16.268 38 24 38ZM24 29.6C27.0928 29.6 29.6 27.0928 29.6 24C29.6 20.9072 27.0928 18.4 24 18.4C20.9072 18.4 18.4 20.9072 18.4 24C18.4 27.0928 20.9072 29.6 24 29.6Z" fill="url(#paint0_linear_289_11393)"/>
|
||||
<path d="M16.5 36C16.5 34.067 18.067 32.5 20 32.5H38C39.933 32.5 41.5 34.067 41.5 36V38C41.5 39.933 39.933 41.5 38 41.5H20C18.067 41.5 16.5 39.933 16.5 38V36Z" fill="#E6DEFF" stroke="#9294D0"/>
|
||||
<circle cx="23" cy="37" r="2" fill="#3A3B59"/>
|
||||
<circle cx="29" cy="37" r="2" fill="#3A3B59"/>
|
||||
<circle cx="35" cy="37" r="2" fill="#3A3B59"/>
|
||||
</g>
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<path d="M44 40C44 42.2091 42.2091 44 40 44H20C17.7909 44 16 42.2091 16 40V26C16 23.7909 17.7909 22 20 22H37.3431C38.404 22 39.4214 21.5786 40.1716 20.8284L41.8027 19.1973L42.9014 18.0986C43.3068 17.6932 44 17.9803 44 18.5537V40Z" fill="url(#paint0_linear_4272_17342)"/>
|
||||
<path d="M4 8C4 5.79086 5.79086 4 8 4H31C33.2091 4 35 5.79086 35 8V14C35 16.2091 33.2091 18 31 18H10.6569C9.59599 18 8.57857 18.4214 7.82843 19.1716L6.5 20.5L5.25 21.75C4.78872 22.2113 4 21.8846 4 21.2322V8Z" fill="url(#paint1_linear_4272_17342)"/>
|
||||
<path d="M11.4629 14.1904C13.3184 14.1904 14.4268 13.2725 14.4268 11.832V11.8271C14.4268 10.6895 13.7432 10.0889 12.2441 9.7959L11.5166 9.6543C10.7549 9.50293 10.4326 9.2832 10.4326 8.88281V8.87793C10.4326 8.45801 10.8135 8.16992 11.458 8.16992C12.1074 8.16992 12.5469 8.46289 12.6201 8.90234L12.6299 8.96094H14.29L14.2852 8.90234C14.1777 7.65723 13.2158 6.76367 11.4482 6.76367C9.82715 6.76367 8.65527 7.65234 8.65039 9.03418V9.03906C8.65039 10.1377 9.29492 10.8408 10.7842 11.1289L11.5117 11.2705C12.3369 11.4316 12.6494 11.6172 12.6494 12.0127V12.0176C12.6494 12.4668 12.1904 12.7842 11.4922 12.7842C10.7695 12.7842 10.2861 12.4717 10.2373 12.0371L10.2324 11.9932H8.52344L8.52832 12.0811C8.60156 13.3896 9.70508 14.1904 11.4629 14.1904Z" fill="#302F38"/>
|
||||
<path d="M15.7842 14H17.3613V9.5127H17.459L19.0605 14H20.0811L21.6826 9.5127H21.7852V14H23.3574V6.9541H21.3213L19.6221 11.7344H19.5293L17.8252 6.9541H15.7842V14Z" fill="#302F38"/>
|
||||
<path d="M27.6543 14.1904C29.5098 14.1904 30.6182 13.2725 30.6182 11.832V11.8271C30.6182 10.6895 29.9346 10.0889 28.4355 9.7959L27.708 9.6543C26.9463 9.50293 26.624 9.2832 26.624 8.88281V8.87793C26.624 8.45801 27.0049 8.16992 27.6494 8.16992C28.2988 8.16992 28.7383 8.46289 28.8115 8.90234L28.8213 8.96094H30.4814L30.4766 8.90234C30.3691 7.65723 29.4072 6.76367 27.6396 6.76367C26.0186 6.76367 24.8467 7.65234 24.8418 9.03418V9.03906C24.8418 10.1377 25.4863 10.8408 26.9756 11.1289L27.7031 11.2705C28.5283 11.4316 28.8408 11.6172 28.8408 12.0127V12.0176C28.8408 12.4668 28.3818 12.7842 27.6836 12.7842C26.9609 12.7842 26.4775 12.4717 26.4287 12.0371L26.4238 11.9932H24.7148L24.7197 12.0811C24.793 13.3896 25.8965 14.1904 27.6543 14.1904Z" fill="#302F38"/>
|
||||
<circle cx="41" cy="7" r="1.5" stroke="#78767F"/>
|
||||
<rect x="6.87898" y="38" width="3" height="3" rx="0.5" transform="rotate(-45 6.87898 38)" stroke="#78767F"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.3526 25.2345C36.9301 25.0827 36.4747 25 36 25H24C23.5258 25 23.0709 25.0825 22.6488 25.234L30.001 32.5861L37.3526 25.2345ZM30.001 34.9485C30.2735 34.9622 30.5505 34.865 30.7586 34.6569L39.0286 26.3869C39.6339 27.0879 40 28.0012 40 29V37C40 39.2091 38.2091 41 36 41H24C21.7909 41 20 39.2091 20 37V29C20 28.0007 20.3665 27.087 20.9723 26.3859L29.2433 34.6569C29.4514 34.865 29.7285 34.9622 30.001 34.9485Z" fill="#F5EEFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_11393" x1="7.7" y1="26.0417" x2="37.6276" y2="14.9915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_4272_17342" x1="20" y1="44" x2="40.6018" y2="19.2523" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4272_17342" x1="29.9948" y1="6.17708" x2="20.199" y2="26.0899" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F07EFF"/>
|
||||
<stop offset="1" stop-color="#FFF480"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_289_11393">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
@ -1,21 +1,23 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3406_128288)">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="#F3EFFA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 8C21 6.89543 21.8954 6 23 6H25C26.1046 6 27 6.89543 27 8V12H21V8ZM21 40C21 41.1046 21.8954 42 23 42H25C26.1046 42 27 41.1046 27 40V36H21V40ZM8 27C6.89543 27 6 26.1046 6 25V23C6 21.8954 6.89543 21 8 21H12V27H8ZM40 27C41.1046 27 42 26.1046 42 25V23C42 21.8954 41.1046 21 40 21H36V27H40Z" fill="#7958FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5638 14.8076C9.78274 14.0266 9.78274 12.7602 10.5638 11.9792L11.978 10.565C12.759 9.78392 14.0254 9.78392 14.8064 10.565L17.6349 13.3934L13.3922 17.636L10.5638 14.8076ZM33.1912 37.435C33.9722 38.2161 35.2386 38.2161 36.0196 37.435L37.4338 36.0208C38.2149 35.2398 38.2149 33.9734 37.4338 33.1924L34.6054 30.364L30.3628 34.6066L33.1912 37.435ZM14.8064 37.435C14.0254 38.2161 12.759 38.2161 11.978 37.435L10.5638 36.0208C9.78274 35.2398 9.78274 33.9734 10.5638 33.1924L13.3922 30.364L17.6349 34.6066L14.8064 37.435ZM37.4338 14.8076C38.2149 14.0266 38.2149 12.7602 37.4338 11.9792L36.0196 10.565C35.2386 9.78392 33.9722 9.78392 33.1912 10.565L30.3628 13.3934L34.6054 17.636L37.4338 14.8076Z" fill="#7958FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 38C31.732 38 38 31.732 38 24C38 16.268 31.732 10 24 10C16.268 10 10 16.268 10 24C10 31.732 16.268 38 24 38ZM24 29.6C27.0928 29.6 29.6 27.0928 29.6 24C29.6 20.9072 27.0928 18.4 24 18.4C20.9072 18.4 18.4 20.9072 18.4 24C18.4 27.0928 20.9072 29.6 24 29.6Z" fill="url(#paint0_linear_3406_128288)"/>
|
||||
<path d="M16.5 36C16.5 34.067 18.067 32.5 20 32.5H38C39.933 32.5 41.5 34.067 41.5 36V38C41.5 39.933 39.933 41.5 38 41.5H20C18.067 41.5 16.5 39.933 16.5 38V36Z" fill="#F5EEFF" stroke="black"/>
|
||||
<circle cx="23" cy="37" r="2" fill="#2D3132"/>
|
||||
<circle cx="29" cy="37" r="2" fill="#2D3132"/>
|
||||
<circle cx="35" cy="37" r="2" fill="#2D3132"/>
|
||||
</g>
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<path d="M44 40C44 42.2091 42.2091 44 40 44H20C17.7909 44 16 42.2091 16 40V26C16 23.7909 17.7909 22 20 22H37.3431C38.404 22 39.4214 21.5786 40.1716 20.8284L41.8027 19.1973L42.9014 18.0986C43.3068 17.6932 44 17.9803 44 18.5537V40Z" fill="url(#paint0_linear_1555_9231)"/>
|
||||
<path d="M4 8C4 5.79086 5.79086 4 8 4H31C33.2091 4 35 5.79086 35 8V14C35 16.2091 33.2091 18 31 18H10.6569C9.59599 18 8.57857 18.4214 7.82843 19.1716L6.5 20.5L5.25 21.75C4.78872 22.2113 4 21.8846 4 21.2322V8Z" fill="url(#paint1_linear_1555_9231)"/>
|
||||
<path d="M11.4629 14.1904C13.3184 14.1904 14.4268 13.2725 14.4268 11.832V11.8271C14.4268 10.6895 13.7432 10.0889 12.2441 9.7959L11.5166 9.6543C10.7549 9.50293 10.4326 9.2832 10.4326 8.88281V8.87793C10.4326 8.45801 10.8135 8.16992 11.458 8.16992C12.1074 8.16992 12.5469 8.46289 12.6201 8.90234L12.6299 8.96094H14.29L14.2852 8.90234C14.1777 7.65723 13.2158 6.76367 11.4482 6.76367C9.82715 6.76367 8.65527 7.65234 8.65039 9.03418V9.03906C8.65039 10.1377 9.29492 10.8408 10.7842 11.1289L11.5117 11.2705C12.3369 11.4316 12.6494 11.6172 12.6494 12.0127V12.0176C12.6494 12.4668 12.1904 12.7842 11.4922 12.7842C10.7695 12.7842 10.2861 12.4717 10.2373 12.0371L10.2324 11.9932H8.52344L8.52832 12.0811C8.60156 13.3896 9.70508 14.1904 11.4629 14.1904Z" fill="#302F38"/>
|
||||
<path d="M15.7842 14H17.3613V9.5127H17.459L19.0605 14H20.0811L21.6826 9.5127H21.7852V14H23.3574V6.9541H21.3213L19.6221 11.7344H19.5293L17.8252 6.9541H15.7842V14Z" fill="#302F38"/>
|
||||
<path d="M27.6543 14.1904C29.5098 14.1904 30.6182 13.2725 30.6182 11.832V11.8271C30.6182 10.6895 29.9346 10.0889 28.4355 9.7959L27.708 9.6543C26.9463 9.50293 26.624 9.2832 26.624 8.88281V8.87793C26.624 8.45801 27.0049 8.16992 27.6494 8.16992C28.2988 8.16992 28.7383 8.46289 28.8115 8.90234L28.8213 8.96094H30.4814L30.4766 8.90234C30.3691 7.65723 29.4072 6.76367 27.6396 6.76367C26.0186 6.76367 24.8467 7.65234 24.8418 9.03418V9.03906C24.8418 10.1377 25.4863 10.8408 26.9756 11.1289L27.7031 11.2705C28.5283 11.4316 28.8408 11.6172 28.8408 12.0127V12.0176C28.8408 12.4668 28.3818 12.7842 27.6836 12.7842C26.9609 12.7842 26.4775 12.4717 26.4287 12.0371L26.4238 11.9932H24.7148L24.7197 12.0811C24.793 13.3896 25.8965 14.1904 27.6543 14.1904Z" fill="#302F38"/>
|
||||
<circle cx="41" cy="7" r="1.5" stroke="#F7F8F8"/>
|
||||
<circle cx="41" cy="7" r="1.5" stroke="#78767F" stroke-opacity="0.02"/>
|
||||
<circle cx="41" cy="7" r="1.5" stroke="#5D34F2" stroke-opacity="0.14"/>
|
||||
<rect x="6.87898" y="38" width="3" height="3" rx="0.5" transform="rotate(-45 6.87898 38)" stroke="#FFD5FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.3526 25.2345C36.9301 25.0827 36.4747 25 36 25H24C23.5258 25 23.0709 25.0825 22.6488 25.234L30.001 32.5861L37.3526 25.2345ZM30.001 34.9485C30.2735 34.9622 30.5505 34.865 30.7586 34.6569L39.0286 26.3869C39.6339 27.0879 40 28.0012 40 29V37C40 39.2091 38.2091 41 36 41H24C21.7909 41 20 39.2091 20 37V29C20 28.0007 20.3665 27.087 20.9723 26.3859L29.2433 34.6569C29.4514 34.865 29.7285 34.9622 30.001 34.9485Z" fill="#F5EEFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3406_128288" x1="7.7" y1="26.0417" x2="37.6276" y2="14.9915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#492EF3"/>
|
||||
<stop offset="1" stop-color="#CF69FF"/>
|
||||
<linearGradient id="paint0_linear_1555_9231" x1="20" y1="44" x2="40.6018" y2="19.2523" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1555_9231" x1="29.9948" y1="6.17708" x2="20.199" y2="26.0899" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F07EFF"/>
|
||||
<stop offset="1" stop-color="#FFF480"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3406_128288">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H40C44.4183 0 48 3.58172 48 8V40C48 44.4183 44.4183 48 40 48H8C3.58172 48 0 44.4183 0 40V8Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.5 KiB |
28
packages/console/src/assets/images/social-dark.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#3A3B59"/>
|
||||
<path d="M26 24L29 18M26 24H28.9415C30.2333 24 31.4456 23.3761 32.1965 22.325L34.8035 18.675C35.5544 17.6239 36.7667 17 38.0585 17H41M26 24V27M26 27H30.7352C32.1403 27 33.4423 27.7372 34.1652 28.942L34.8348 30.058C35.5577 31.2628 36.8597 32 38.2648 32H41M26 27V32" stroke="#CABEFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24V9M24 24H20.2819C19.4482 24 18.6353 23.7395 17.9569 23.2549L13.0431 19.7451C12.3647 19.2605 11.5518 19 10.7181 19H10M24 24V38M16 27L11.95 29.43C11.3284 29.803 10.617 30 9.89206 30H7" stroke="#CABEFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="24" cy="24" r="12" fill="url(#paint0_linear_4272_17343)"/>
|
||||
<mask id="mask0_4272_17343" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="12" y="12" width="24" height="24">
|
||||
<circle cx="24" cy="24" r="12" fill="url(#paint1_linear_4272_17343)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4272_17343)">
|
||||
<path d="M23.5078 16C23.1315 16 22.806 16.2623 22.7261 16.6301L22.4584 17.8613C22.396 18.1485 22.1795 18.3741 21.9046 18.4781C21.7934 18.5202 21.6837 18.5656 21.5758 18.6141C21.3075 18.7346 20.9946 18.7283 20.7472 18.5692L19.6912 17.8903C19.3746 17.6868 18.959 17.7315 18.6929 17.9976L17.9976 18.6929C17.7315 18.959 17.6868 19.3746 17.8903 19.6912L18.5692 20.7473C18.7283 20.9946 18.7346 21.3075 18.6141 21.5758C18.5656 21.6837 18.5202 21.7934 18.4781 21.9046C18.3741 22.1795 18.1484 22.396 17.8613 22.4584L16.6301 22.7261C16.2623 22.806 16 23.1315 16 23.5078V24.4922C16 24.8685 16.2623 25.194 16.6301 25.2739L17.8613 25.5416C18.1485 25.604 18.3741 25.8205 18.4781 26.0954C18.5202 26.2066 18.5656 26.3163 18.6141 26.4242C18.7346 26.6925 18.7283 27.0054 18.5692 27.2528L17.8903 28.3088C17.6868 28.6254 17.7315 29.041 17.9976 29.3071L18.6929 30.0024C18.959 30.2685 19.3746 30.3132 19.6912 30.1097L20.7473 29.4308C20.9946 29.2717 21.3075 29.2654 21.5758 29.3859C21.6837 29.4344 21.7934 29.4798 21.9046 29.5219C22.1795 29.6259 22.396 29.8516 22.4584 30.1387L22.7261 31.3699C22.806 31.7377 23.1315 32 23.5078 32H24.4922C24.8685 32 25.194 31.7377 25.2739 31.3699L25.5416 30.1387C25.604 29.8515 25.8205 29.6259 26.0954 29.5219C26.2066 29.4798 26.3163 29.4344 26.4242 29.3859C26.6925 29.2654 27.0054 29.2717 27.2528 29.4308L28.3088 30.1097C28.6254 30.3132 29.041 30.2685 29.3071 30.0024L30.0024 29.3071C30.2685 29.041 30.3132 28.6254 30.1097 28.3088L29.4308 27.2527C29.2717 27.0054 29.2654 26.6925 29.3859 26.4242C29.4344 26.3163 29.4798 26.2066 29.5219 26.0954C29.6259 25.8205 29.8516 25.604 30.1387 25.5416L31.3699 25.2739C31.7377 25.194 32 24.8685 32 24.4922V23.5078C32 23.1315 31.7377 22.806 31.3699 22.7261L30.1387 22.4584C29.8515 22.396 29.6259 22.1795 29.5219 21.9046C29.4798 21.7934 29.4344 21.6837 29.3859 21.5758C29.2654 21.3075 29.2717 20.9946 29.4308 20.7472L30.1097 19.6912C30.3132 19.3746 30.2685 18.959 30.0024 18.6929L29.3071 17.9976C29.041 17.7315 28.6254 17.6868 28.3088 17.8903L27.2527 18.5692C27.0054 18.7283 26.6925 18.7346 26.4242 18.6141C26.3163 18.5656 26.2066 18.5202 26.0954 18.4781C25.8205 18.3741 25.604 18.1484 25.5416 17.8613L25.2739 16.6301C25.194 16.2623 24.8685 16 24.4922 16H23.5078ZM24 20.5714C25.8936 20.5714 27.4286 22.1065 27.4286 24C27.4286 25.8936 25.8936 27.4286 24 27.4286C22.1064 27.4286 20.5714 25.8936 20.5714 24C20.5714 22.1065 22.1064 20.5714 24 20.5714Z" fill="#F5EEFF"/>
|
||||
</g>
|
||||
<circle cx="42" cy="17" r="2" fill="#FAABFF"/>
|
||||
<circle cx="7" cy="19" r="3" fill="#83DA85"/>
|
||||
<circle cx="24" cy="7" r="3" fill="#AF9EFF"/>
|
||||
<circle cx="41" cy="32" r="3" fill="#947DFF"/>
|
||||
<circle cx="7" cy="30" r="3" fill="#AF9EFF"/>
|
||||
<circle cx="24" cy="41" r="3" fill="#FAABFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4272_17343" x1="16.125" y1="32.75" x2="32.25" y2="15.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4272_17343" x1="16.125" y1="32.75" x2="32.25" y2="15.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
28
packages/console/src/assets/images/social.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#F3EFFA"/>
|
||||
<path d="M26 24L29 18M26 24H28.9415C30.2333 24 31.4456 23.3761 32.1965 22.325L34.8035 18.675C35.5544 17.6239 36.7667 17 38.0585 17H41M26 24V27M26 27H30.7352C32.1403 27 33.4423 27.7372 34.1652 28.942L34.8348 30.058C35.5577 31.2628 36.8597 32 38.2648 32H41M26 27V32" stroke="#CABEFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24 24V9M24 24H20.2819C19.4482 24 18.6353 23.7395 17.9569 23.2549L13.0431 19.7451C12.3647 19.2605 11.5518 19 10.7181 19H10M24 24V38M16 27L11.95 29.43C11.3284 29.803 10.617 30 9.89206 30H7" stroke="#CABEFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="24" cy="24" r="12" fill="url(#paint0_linear_1555_9204)"/>
|
||||
<mask id="mask0_1555_9204" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="12" y="12" width="24" height="24">
|
||||
<circle cx="24" cy="24" r="12" fill="url(#paint1_linear_1555_9204)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1555_9204)">
|
||||
<path d="M23.5078 16C23.1315 16 22.806 16.2623 22.7261 16.6301L22.4584 17.8613C22.396 18.1485 22.1795 18.3741 21.9046 18.4781C21.7934 18.5202 21.6837 18.5656 21.5758 18.6141C21.3075 18.7346 20.9946 18.7283 20.7472 18.5692L19.6912 17.8903C19.3746 17.6868 18.959 17.7315 18.6929 17.9976L17.9976 18.6929C17.7315 18.959 17.6868 19.3746 17.8903 19.6912L18.5692 20.7473C18.7283 20.9946 18.7346 21.3075 18.6141 21.5758C18.5656 21.6837 18.5202 21.7934 18.4781 21.9046C18.3741 22.1795 18.1484 22.396 17.8613 22.4584L16.6301 22.7261C16.2623 22.806 16 23.1315 16 23.5078V24.4922C16 24.8685 16.2623 25.194 16.6301 25.2739L17.8613 25.5416C18.1485 25.604 18.3741 25.8205 18.4781 26.0954C18.5202 26.2066 18.5656 26.3163 18.6141 26.4242C18.7346 26.6925 18.7283 27.0054 18.5692 27.2528L17.8903 28.3088C17.6868 28.6254 17.7315 29.041 17.9976 29.3071L18.6929 30.0024C18.959 30.2685 19.3746 30.3132 19.6912 30.1097L20.7473 29.4308C20.9946 29.2717 21.3075 29.2654 21.5758 29.3859C21.6837 29.4344 21.7934 29.4798 21.9046 29.5219C22.1795 29.6259 22.396 29.8516 22.4584 30.1387L22.7261 31.3699C22.806 31.7377 23.1315 32 23.5078 32H24.4922C24.8685 32 25.194 31.7377 25.2739 31.3699L25.5416 30.1387C25.604 29.8515 25.8205 29.6259 26.0954 29.5219C26.2066 29.4798 26.3163 29.4344 26.4242 29.3859C26.6925 29.2654 27.0054 29.2717 27.2528 29.4308L28.3088 30.1097C28.6254 30.3132 29.041 30.2685 29.3071 30.0024L30.0024 29.3071C30.2685 29.041 30.3132 28.6254 30.1097 28.3088L29.4308 27.2527C29.2717 27.0054 29.2654 26.6925 29.3859 26.4242C29.4344 26.3163 29.4798 26.2066 29.5219 26.0954C29.6259 25.8205 29.8516 25.604 30.1387 25.5416L31.3699 25.2739C31.7377 25.194 32 24.8685 32 24.4922V23.5078C32 23.1315 31.7377 22.806 31.3699 22.7261L30.1387 22.4584C29.8515 22.396 29.6259 22.1795 29.5219 21.9046C29.4798 21.7934 29.4344 21.6837 29.3859 21.5758C29.2654 21.3075 29.2717 20.9946 29.4308 20.7472L30.1097 19.6912C30.3132 19.3746 30.2685 18.959 30.0024 18.6929L29.3071 17.9976C29.041 17.7315 28.6254 17.6868 28.3088 17.8903L27.2527 18.5692C27.0054 18.7283 26.6925 18.7346 26.4242 18.6141C26.3163 18.5656 26.2066 18.5202 26.0954 18.4781C25.8205 18.3741 25.604 18.1484 25.5416 17.8613L25.2739 16.6301C25.194 16.2623 24.8685 16 24.4922 16H23.5078ZM24 20.5714C25.8936 20.5714 27.4286 22.1065 27.4286 24C27.4286 25.8936 25.8936 27.4286 24 27.4286C22.1064 27.4286 20.5714 25.8936 20.5714 24C20.5714 22.1065 22.1064 20.5714 24 20.5714Z" fill="#F5EEFF"/>
|
||||
</g>
|
||||
<circle cx="42" cy="17" r="2" fill="#FAABFF"/>
|
||||
<circle cx="7" cy="19" r="3" fill="#83DA85"/>
|
||||
<circle cx="24" cy="7" r="3" fill="#AF9EFF"/>
|
||||
<circle cx="41" cy="32" r="3" fill="#947DFF"/>
|
||||
<circle cx="7" cy="30" r="3" fill="#AF9EFF"/>
|
||||
<circle cx="24" cy="41" r="3" fill="#FAABFF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1555_9204" x1="16.125" y1="32.75" x2="32.25" y2="15.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1555_9204" x1="16.125" y1="32.75" x2="32.25" y2="15.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -1,12 +1,13 @@
|
|||
import { useLogto, IdTokenClaims } from '@logto/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef, useState, MouseEvent } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
import { Ring as Spinner } from '@/components/Spinner';
|
||||
import { generateAvatarPlaceHolderById } from '@/consts/avatars';
|
||||
import SignOut from '@/icons/SignOut';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import UserInfoSkeleton from '../UserInfoSkeleton';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -43,13 +44,18 @@ const UserInfo = () => {
|
|||
<>
|
||||
<div
|
||||
ref={anchorRef}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.container, showDropdown && styles.active)}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
setShowDropdown(true);
|
||||
})}
|
||||
onClick={() => {
|
||||
setShowDropdown(true);
|
||||
}}
|
||||
>
|
||||
{/* TODO: revert after SDK updated */}
|
||||
<img src={picture ? String(picture) : generateAvatarPlaceHolderById(id)} />
|
||||
<img src={picture ? String(picture) : generateAvatarPlaceHolderById(id)} alt="avatar" />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.name}>{username}</div>
|
||||
</div>
|
||||
|
@ -66,7 +72,7 @@ const UserInfo = () => {
|
|||
<DropdownItem
|
||||
className={classNames(styles.dropdownItem, isLoading && styles.loading)}
|
||||
icon={<SignOut className={styles.signOutIcon} />}
|
||||
onClick={(event: MouseEvent<HTMLLIElement>) => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import ErrorDark from '@/assets/images/error-dark.svg';
|
|||
import Error from '@/assets/images/error.svg';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -33,7 +34,12 @@ const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props
|
|||
{errorMessage}
|
||||
{callStack && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.expander}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
setIsDetailsOpen(!isDetailsOpen);
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsDetailsOpen(!isDetailsOpen);
|
||||
}}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { TFuncKey, useTranslation } from 'react-i18next';
|
|||
import Copy from '@/icons/Copy';
|
||||
import Eye from '@/icons/Eye';
|
||||
import EyeClosed from '@/icons/EyeClosed';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import IconButton from '../IconButton';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
@ -57,6 +58,11 @@ const CopyToClipboard = ({
|
|||
return (
|
||||
<div
|
||||
className={classNames(styles.container, styles[variant], className)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler((event) => {
|
||||
event.stopPropagation();
|
||||
})}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
|
|
@ -40,6 +40,7 @@ const DeleteConfirmModal = ({
|
|||
{children}
|
||||
{expectedInput && (
|
||||
<TextInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={input}
|
||||
placeholder={inputPlaceholder}
|
||||
|
|
|
@ -20,6 +20,8 @@ const Drawer = ({ title, subtitle, isOpen, children, onClose }: Props) => {
|
|||
return (
|
||||
<ReactModal
|
||||
shouldCloseOnOverlayClick
|
||||
// Styling purpose
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
role="drawer"
|
||||
isOpen={isOpen}
|
||||
className={styles.content}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import classNames from 'classnames';
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { MouseEvent, KeyboardEvent, ReactNode } from 'react';
|
||||
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './DropdownItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
onClick?: (event: MouseEvent<HTMLLIElement>) => void;
|
||||
onClick?: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
||||
className?: string;
|
||||
children: ReactNode | Record<string, unknown>;
|
||||
icon?: ReactNode;
|
||||
|
@ -20,7 +22,13 @@ const DropdownItem = ({
|
|||
iconClassName,
|
||||
type = 'default',
|
||||
}: Props) => (
|
||||
<li className={classNames(styles.item, styles[type], className)} onClick={onClick}>
|
||||
<li
|
||||
role="menuitem"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.item, styles[type], className)}
|
||||
onKeyDown={onKeyDownHandler(onClick)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon && <span className={classNames(styles.icon, iconClassName)}>{icon}</span>}
|
||||
{children}
|
||||
</li>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ReactNode, RefObject, useRef } from 'react';
|
|||
import ReactModal from 'react-modal';
|
||||
|
||||
import usePosition, { HorizontalAlignment } from '@/hooks/use-position';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -61,7 +62,13 @@ const Dropdown = ({
|
|||
>
|
||||
<div ref={overlayRef} className={styles.dropdownContainer}>
|
||||
{title && <div className={classNames(styles.title, titleClassName)}>{title}</div>}
|
||||
<ul className={classNames(styles.list, className)} onClick={onClose}>
|
||||
<ul
|
||||
className={classNames(styles.list, className)}
|
||||
role="menu"
|
||||
tabIndex={0}
|
||||
onClick={onClose}
|
||||
onKeyDown={onKeyDownHandler({ Enter: onClose, Esc: onClose })}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||
import Minus from '@/icons/Minus';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import ConfirmModal from '../ConfirmModal';
|
||||
import IconButton from '../IconButton';
|
||||
|
@ -85,7 +86,13 @@ const MultiTextInput = ({ title, value, onChange, onKeyPress, error, placeholder
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className={classNames(textButtonStyles.button, styles.addAnother)} onClick={handleAdd}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(textButtonStyles.button, styles.addAnother)}
|
||||
onKeyDown={onKeyDownHandler(handleAdd)}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{t('general.add_another')}
|
||||
</div>
|
||||
<ConfirmModal
|
||||
|
|
|
@ -29,6 +29,7 @@ export type Props = {
|
|||
type?: 'card' | 'plain';
|
||||
isDisabled?: boolean;
|
||||
disabledLabel?: AdminConsoleKey;
|
||||
size?: 'normal' | 'small';
|
||||
};
|
||||
|
||||
const Radio = ({
|
||||
|
@ -43,6 +44,7 @@ const Radio = ({
|
|||
type,
|
||||
isDisabled,
|
||||
disabledLabel,
|
||||
size = 'normal',
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
|
@ -64,10 +66,13 @@ const Radio = ({
|
|||
<div
|
||||
className={classNames(
|
||||
styles.radio,
|
||||
className,
|
||||
isChecked && styles.checked,
|
||||
isDisabled && styles.disabled
|
||||
isDisabled && styles.disabled,
|
||||
styles[size],
|
||||
className
|
||||
)}
|
||||
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
|
||||
role="radio"
|
||||
tabIndex={tabIndex}
|
||||
onClick={isDisabled ? undefined : onClick}
|
||||
onKeyPress={handleKeyPress}
|
||||
|
|
|
@ -52,10 +52,19 @@
|
|||
top: _.unit(5);
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: _.unit(3);
|
||||
|
||||
svg {
|
||||
right: _.unit(3);
|
||||
top: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
||||
.disabledLabel {
|
||||
position: absolute;
|
||||
right: _.unit(5);
|
||||
top: _.unit(5);
|
||||
right: _.unit(3);
|
||||
top: _.unit(3);
|
||||
background: var(--color-neutral-90);
|
||||
padding: _.unit(0.5) _.unit(2);
|
||||
border-radius: 10px;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ReactEventHandler, ReactNode, useRef, useState } from 'react';
|
|||
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
import Close from '@/icons/Close';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import Dropdown, { DropdownItem } from '../Dropdown';
|
||||
import IconButton from '../IconButton';
|
||||
|
@ -65,6 +66,12 @@ const Select = <T extends string>({
|
|||
className
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
if (!isReadOnly) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isReadOnly) {
|
||||
setIsOpen(true);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './TabNavItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -16,7 +18,14 @@ const TabNavItem = ({ children, href, isActive, onClick }: Props) => {
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.link, selected && styles.selected)}>
|
||||
{href ? <Link to={href}>{children}</Link> : <a onClick={onClick}>{children}</a>}
|
||||
{href ? (
|
||||
<Link to={href}>{children}</Link>
|
||||
) : (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a role="tab" tabIndex={0} onKeyDown={onKeyDownHandler(onClick)} onClick={onClick}>
|
||||
{children}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import classNames from 'classnames';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useState, useCallback } from 'react';
|
||||
import AnimateHeight, { Height } from 'react-animate-height';
|
||||
|
||||
import ArrowRight from '@/assets/images/triangle-right.svg';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -15,14 +16,26 @@ const DetailsSummary = ({ children }: Props) => {
|
|||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [height, setHeight] = useState<Height>(0);
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
setHeight(height === 0 ? 'auto' : 0);
|
||||
}, [height, isExpanded]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, isExpanded && styles.expanded)}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.summary}
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
setHeight(height === 0 ? 'auto' : 0);
|
||||
}}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Esc: () => {
|
||||
setIsExpanded(false);
|
||||
setHeight(0);
|
||||
},
|
||||
Enter: onClickHandler,
|
||||
' ': onClickHandler,
|
||||
})}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
<ArrowRight className={styles.arrow} />
|
||||
{summary}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AdminConsoleKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
|
||||
import { PropsWithChildren, useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
|
@ -10,6 +10,7 @@ import IconButton from '@/components/IconButton';
|
|||
import Index from '@/components/Index';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -54,13 +55,24 @@ const Step = ({
|
|||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
setIsExpanded((expand) => !expand);
|
||||
}, [setIsExpanded]);
|
||||
|
||||
return (
|
||||
<Card key={title} ref={ref} className={styles.card}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.header}
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Esc: () => {
|
||||
setIsExpanded(false);
|
||||
},
|
||||
Enter: onToggle,
|
||||
' ': onToggle,
|
||||
})}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Index
|
||||
className={styles.index}
|
||||
|
|
|
@ -55,6 +55,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
<form>
|
||||
<FormField isRequired title="api_resources.api_name">
|
||||
<TextInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
{...register('name', { required: true })}
|
||||
placeholder={t('api_resources.api_name_placeholder')}
|
||||
|
|
|
@ -18,6 +18,7 @@ import Drawer from '@/components/Drawer';
|
|||
import LinkButton from '@/components/LinkButton';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import Back from '@/icons/Back';
|
||||
import Delete from '@/icons/Delete';
|
||||
import More from '@/icons/More';
|
||||
|
@ -54,6 +55,7 @@ const ApplicationDetails = () => {
|
|||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const formMethods = useForm<Application>();
|
||||
const documentationUrl = useDocumentationUrl();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
|
@ -145,15 +147,21 @@ const ApplicationDetails = () => {
|
|||
</div>
|
||||
<div className={styles.operations}>
|
||||
{/* TODO: @Charles figure out a better way to check guide availability */}
|
||||
{data.type !== ApplicationType.MachineToMachine && (
|
||||
<Button
|
||||
title="application_details.check_guide"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setIsReadmeOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
title="application_details.check_guide"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if (data.type === ApplicationType.MachineToMachine) {
|
||||
window.open(
|
||||
`${documentationUrl}/docs/recipes/integrate-logto/machine-to-machine/`,
|
||||
'_blank'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
setIsReadmeOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Drawer isOpen={isReadmeOpen} onClose={onCloseDrawer}>
|
||||
<Guide isCompact app={data} onClose={onCloseDrawer} />
|
||||
</Drawer>
|
||||
|
|
|
@ -88,6 +88,7 @@ const ConnectorDetails = () => {
|
|||
<div className={styles.logoContainer}>
|
||||
<img
|
||||
src={theme === AppearanceMode.DarkMode && data.logoDark ? data.logoDark : data.logo}
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -75,6 +75,7 @@ const ConnectorName = ({ type, connectors, onClickSetup }: Props) => {
|
|||
<div className={styles.logoContainer}>
|
||||
<img
|
||||
className={styles.logo}
|
||||
alt="logo"
|
||||
src={
|
||||
theme === AppearanceMode.DarkMode && connector.logoDark
|
||||
? connector.logoDark
|
||||
|
|
|
@ -1,43 +1,54 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.body {
|
||||
padding-top: _.unit(5);
|
||||
.connectorGroup {
|
||||
gap: _.unit(4);
|
||||
|
||||
.connector {
|
||||
font: var(--font-body-medium);
|
||||
width: 230px;
|
||||
.connectorRadio {
|
||||
// Override radio style
|
||||
min-width: 276px;
|
||||
max-width: 276px;
|
||||
width: 276px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-hover);
|
||||
.connector {
|
||||
font: var(--font-body-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: _.unit(2);
|
||||
font: var(--font-subhead-2);
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
.connectorId {
|
||||
margin-top: _.unit(2);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-neutral-50);
|
||||
}
|
||||
.name {
|
||||
font: var(--font-subhead-2);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
margin-top: _.unit(2);
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
.connectorId {
|
||||
margin-top: _.unit(1);
|
||||
font: var(--font-body-small);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-small);
|
||||
color: var(--color-caption);
|
||||
margin-top: _.unit(1);
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,8 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
|
|||
const closeModal = () => {
|
||||
setIsGetStartedModalOpen(false);
|
||||
onClose?.(activeConnectorId);
|
||||
setActiveGroupId(undefined);
|
||||
setActiveConnectorId(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -95,30 +97,43 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
|
|||
/>
|
||||
}
|
||||
className={styles.body}
|
||||
size="large"
|
||||
size="xlarge"
|
||||
onClose={onClose}
|
||||
>
|
||||
{isLoading && 'Loading...'}
|
||||
{error?.message}
|
||||
{groups && (
|
||||
<RadioGroup name="group" value={activeGroupId} type="card" onChange={handleGroupChange}>
|
||||
<RadioGroup
|
||||
name="group"
|
||||
value={activeGroupId}
|
||||
type="card"
|
||||
className={styles.connectorGroup}
|
||||
onChange={handleGroupChange}
|
||||
>
|
||||
{groups.map(({ id, name, logo, description, connectors }) => (
|
||||
<Radio
|
||||
key={id}
|
||||
value={id}
|
||||
className={styles.connectorRadio}
|
||||
isDisabled={connectors.every(({ enabled }) => enabled)}
|
||||
className={styles.connector}
|
||||
disabledLabel="general.added"
|
||||
size="small"
|
||||
>
|
||||
<div className={styles.logo}>
|
||||
<img src={logo} />
|
||||
</div>
|
||||
<div className={styles.name}>
|
||||
<UnnamedTrans resource={name} />
|
||||
</div>
|
||||
{type !== ConnectorType.Social && <div className={styles.connectorId}>{id}</div>}
|
||||
<div className={styles.description}>
|
||||
<UnnamedTrans resource={description} />
|
||||
<div className={styles.connector}>
|
||||
<div className={styles.logo}>
|
||||
<img src={logo} alt="logo" />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
<UnnamedTrans resource={name} />
|
||||
</div>
|
||||
{type !== ConnectorType.Social && (
|
||||
<div className={styles.connectorId}>{id}</div>
|
||||
)}
|
||||
<div className={styles.description}>
|
||||
<UnnamedTrans resource={description} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Radio>
|
||||
))}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
|||
import Index from '@/components/Index';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import useGetStartedMetadata from '../../hook';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -28,14 +29,27 @@ const GetStartedProgress = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const showDropDown = () => {
|
||||
setShowDropdown(true);
|
||||
};
|
||||
|
||||
const hideDropDown = () => {
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={anchorRef}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.container, showDropdown && styles.active)}
|
||||
onClick={() => {
|
||||
setShowDropdown(true);
|
||||
}}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Esc: hideDropDown,
|
||||
Enter: showDropDown,
|
||||
' ': showDropDown,
|
||||
})}
|
||||
onClick={showDropDown}
|
||||
>
|
||||
<Icon className={styles.icon} />
|
||||
<span>
|
||||
|
@ -52,9 +66,7 @@ const GetStartedProgress = () => {
|
|||
horizontalAlign="end"
|
||||
title={t('get_started.progress_dropdown_title')}
|
||||
titleClassName={styles.dropdownTitle}
|
||||
onClose={() => {
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
onClose={hideDropDown}
|
||||
>
|
||||
{data.map(({ id, title, isComplete, onClick }, index) => (
|
||||
<DropdownItem
|
||||
|
|
|
@ -13,10 +13,10 @@ import CustomizeDark from '@/assets/images/customize-dark.svg';
|
|||
import Customize from '@/assets/images/customize.svg';
|
||||
import FurtherReadingsDark from '@/assets/images/further-readings-dark.svg';
|
||||
import FurtherReadings from '@/assets/images/further-readings.svg';
|
||||
import OneClickDark from '@/assets/images/one-click-dark.svg';
|
||||
import OneClick from '@/assets/images/one-click.svg';
|
||||
import PasswordlessDark from '@/assets/images/passwordless-dark.svg';
|
||||
import Passwordless from '@/assets/images/passwordless.svg';
|
||||
import SocialDark from '@/assets/images/social-dark.svg';
|
||||
import Social from '@/assets/images/social.svg';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
|
@ -106,7 +106,7 @@ const useGetStartedMetadata = () => {
|
|||
id: 'configureSocialSignIn',
|
||||
title: 'get_started.card5_title',
|
||||
subtitle: 'get_started.card5_subtitle',
|
||||
icon: isLightMode ? OneClick : OneClickDark,
|
||||
icon: isLightMode ? Social : SocialDark,
|
||||
buttonText: 'general.add',
|
||||
isComplete: settings?.socialSignInConfigured,
|
||||
onClick: () => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import Card from '@/components/Card';
|
|||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import Skeleton from './components/Skeleton';
|
||||
import useGetStartedMetadata from './hook';
|
||||
|
@ -26,6 +27,14 @@ const GetStarted = () => {
|
|||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const showConfirmModalHandler = () => {
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
const hideConfirmModalHandler = () => {
|
||||
setShowConfirmModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
|
@ -36,10 +45,15 @@ const GetStarted = () => {
|
|||
<span>
|
||||
{t('get_started.subtitle_part2')}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.hideButton}
|
||||
onClick={() => {
|
||||
setShowConfirmModal(true);
|
||||
}}
|
||||
onClick={showConfirmModalHandler}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Enter: showConfirmModalHandler,
|
||||
' ': showConfirmModalHandler,
|
||||
Esc: hideConfirmModalHandler,
|
||||
})}
|
||||
>
|
||||
{t('get_started.hide_this')}
|
||||
</span>
|
||||
|
@ -70,9 +84,7 @@ const GetStarted = () => {
|
|||
confirmButtonType="primary"
|
||||
confirmButtonText="get_started.hide_this"
|
||||
onConfirm={hideGetStarted}
|
||||
onCancel={() => {
|
||||
setShowConfirmModal(false);
|
||||
}}
|
||||
onCancel={hideConfirmModalHandler}
|
||||
>
|
||||
{t('get_started.confirm_message')}
|
||||
</ConfirmModal>
|
||||
|
|
|
@ -202,12 +202,14 @@ const Preview = ({ signInExperience, className }: Props) => {
|
|||
<PhoneInfo />
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
// The missing of attribute "sandbox" is intended since the source is trusted
|
||||
/* eslint-disable react/iframe-missing-sandbox */
|
||||
}
|
||||
<iframe ref={previewRef} src="/sign-in?preview=true" tabIndex={-1} />
|
||||
{/* eslint-enable react/iframe-missing-sandbox */}
|
||||
<iframe
|
||||
ref={previewRef}
|
||||
// Allow all sandbox rules
|
||||
sandbox={undefined}
|
||||
src="/sign-in?preview=true"
|
||||
tabIndex={-1}
|
||||
title={t('sign_in_exp.preview.title')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@ const ResetPasswordForm = ({ onClose, userId }: Props) => {
|
|||
const onSubmit = async () => {
|
||||
const password = nanoid(8);
|
||||
await api.patch(`/api/users/${userId}/password`, { json: { password } }).json<User>();
|
||||
onClose?.(btoa(password));
|
||||
onClose?.(password);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -113,7 +113,7 @@ const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
|||
<tr key={target}>
|
||||
<td>
|
||||
<div className={styles.connectorName}>
|
||||
<img src={logo} />
|
||||
<img src={logo} alt="logo" />
|
||||
<div className={styles.name}>
|
||||
<UnnamedTrans resource={name} />
|
||||
</div>
|
||||
|
|
|
@ -100,6 +100,7 @@ const UserDetails = () => {
|
|||
className={styles.avatar}
|
||||
src={data.avatar || generateAvatarPlaceHolderById(userId)}
|
||||
referrerPolicy="no-referrer"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div className={styles.metadata}>
|
||||
<div className={styles.name}>{data.name ?? '-'}</div>
|
||||
|
|
|
@ -58,6 +58,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
<form>
|
||||
<FormField isRequired title="users.create_form_username">
|
||||
<TextInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
{...register('username', {
|
||||
required: true,
|
||||
|
|
|
@ -133,6 +133,7 @@ const Users = () => {
|
|||
subtitle={id}
|
||||
icon={
|
||||
<img
|
||||
alt="avatar"
|
||||
className={styles.avatar}
|
||||
src={avatar || generateAvatarPlaceHolderById(id)}
|
||||
/>
|
||||
|
|
21
packages/console/src/utilities/a11y.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { KeyboardEventHandler, KeyboardEvent } from 'react';
|
||||
|
||||
type callbackHandler<T> = ((event: KeyboardEvent<T>) => void) | undefined;
|
||||
|
||||
type callbackHandlerMap<T> = Record<string, callbackHandler<T>>;
|
||||
|
||||
export const onKeyDownHandler =
|
||||
<T = Element>(callback?: callbackHandler<T> | callbackHandlerMap<T>): KeyboardEventHandler<T> =>
|
||||
(event) => {
|
||||
const { key } = event;
|
||||
|
||||
if (typeof callback === 'function' && [' ', 'Enter'].includes(key)) {
|
||||
callback(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (typeof callback === 'object') {
|
||||
callback[key]?.(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
1
packages/core/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/alterations
|
|
@ -3,6 +3,52 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/logto/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-28)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **core:** update `koaAuth()` to inject detailed auth info (#1977)
|
||||
* **core:** update user scopes (#1922)
|
||||
|
||||
### Features
|
||||
|
||||
* **core,phrases:** add check protected access function ([e405ef7](https://github.com/logto-io/logto/commit/e405ef7bb8fdbf01d52ef83b19350189e32a39b6))
|
||||
* **core,schemas:** add phrases schema and GET /custom-phrases/:languageKey route ([#1905](https://github.com/logto-io/logto/issues/1905)) ([7242aa8](https://github.com/logto-io/logto/commit/7242aa8c2bbb70c51e9b00dd5e3aff595c3c2eff))
|
||||
* **core,schemas:** migration deploy cli ([#1966](https://github.com/logto-io/logto/issues/1966)) ([7cc2f4d](https://github.com/logto-io/logto/commit/7cc2f4d14219145e562cebef41ebb3963083cc89))
|
||||
* **core,schemas:** use timestamp to version migrations ([bb4bfd3](https://github.com/logto-io/logto/commit/bb4bfd3d41fdd415f68e6e13f0d4a7e8a0093933))
|
||||
* **core:** add DELETE /custom-phrases/:languageKey route ([#1919](https://github.com/logto-io/logto/issues/1919)) ([c72be69](https://github.com/logto-io/logto/commit/c72be69bea639689721651b20fd559939f6c0ce6))
|
||||
* **core:** add GET /custom-phrases route ([#1935](https://github.com/logto-io/logto/issues/1935)) ([5fe0cf4](https://github.com/logto-io/logto/commit/5fe0cf4257a72f96fc439132c7b5b58e07352aa3))
|
||||
* **core:** add POST /session/forgot-password/{email,sms}/send-passcode ([#1963](https://github.com/logto-io/logto/issues/1963)) ([af2600d](https://github.com/logto-io/logto/commit/af2600d828bf315ce57de5813168571e7042d8de))
|
||||
* **core:** add POST /session/forgot-password/{email,sms}/verify-passcode ([#1968](https://github.com/logto-io/logto/issues/1968)) ([1ea39f3](https://github.com/logto-io/logto/commit/1ea39f346367d9f300be7281a65e689bf198a65c))
|
||||
* **core:** add POST /session/forgot-password/reset ([#1972](https://github.com/logto-io/logto/issues/1972)) ([acdc86c](https://github.com/logto-io/logto/commit/acdc86c8560d30a89eccb6b0f6892221ea1bc5e0))
|
||||
* **core:** add PUT /custom-phrases/:languageKey route ([#1907](https://github.com/logto-io/logto/issues/1907)) ([0ae13f0](https://github.com/logto-io/logto/commit/0ae13f091b69c717cc17ed4f400f456f1737fc5c))
|
||||
* **core:** add ts to interaction result ([#1917](https://github.com/logto-io/logto/issues/1917)) ([e01042c](https://github.com/logto-io/logto/commit/e01042cbcd77c486afa1ee9fc2fa5c1d2df92542))
|
||||
* **core:** cannot delete custom phrase used as default language in sign-in exp ([#1951](https://github.com/logto-io/logto/issues/1951)) ([a1aef26](https://github.com/logto-io/logto/commit/a1aef26905f624569ee47e43bb3a9c9cf05b997b))
|
||||
* **core:** check migration state before app start ([#1979](https://github.com/logto-io/logto/issues/1979)) ([bf1d281](https://github.com/logto-io/logto/commit/bf1d281905bcf91a09dd8330212b6db838d65344))
|
||||
* **core:** deploy migration in transaction mode ([#1980](https://github.com/logto-io/logto/issues/1980)) ([9a89c1a](https://github.com/logto-io/logto/commit/9a89c1a200322c678e2b0246ed324c847e734fc6))
|
||||
* **core:** machine to machine apps ([cd9c697](https://github.com/logto-io/logto/commit/cd9c6978a35d9fc3a571c7bd56c972939c49a9b5))
|
||||
* **core:** save empty string as null value in DB ([#1901](https://github.com/logto-io/logto/issues/1901)) ([ecdf06e](https://github.com/logto-io/logto/commit/ecdf06ef39a177b207dc75930e96dfcf2ae12cdc))
|
||||
* **core:** support base64 format `OIDC_PRIVATE_KEYS` config in `.env` file ([#1903](https://github.com/logto-io/logto/issues/1903)) ([5bdb675](https://github.com/logto-io/logto/commit/5bdb6755d2e1bf5b6a004859561d60f1103aec69))
|
||||
* **core:** update migration state after db init ([f904b88](https://github.com/logto-io/logto/commit/f904b88f564110c1ed00b2fa1c7b3c1e168fc106))
|
||||
* **ui:** add passwordless switch ([#1976](https://github.com/logto-io/logto/issues/1976)) ([ddb0e47](https://github.com/logto-io/logto/commit/ddb0e47950b3bd7f92af2a8a5e14b201e0a10ed7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump react sdk and essentials toolkit to support CJK characters in idToken ([2f92b43](https://github.com/logto-io/logto/commit/2f92b438644bd330fa4b8cd3698d9129ecbae282))
|
||||
* **core,schemas:** move alteration types into schemas src ([#2005](https://github.com/logto-io/logto/issues/2005)) ([10c1be6](https://github.com/logto-io/logto/commit/10c1be6eb76e1cb94746aee632a421aea8d4c211))
|
||||
* **core:** filter out connector-kit ([#1987](https://github.com/logto-io/logto/issues/1987)) ([f4cf89f](https://github.com/logto-io/logto/commit/f4cf89fb8deee7472d8e9bdbcb7ae7364ced1f74))
|
||||
* support capital letter "Y" in command line prompt ([416f4e8](https://github.com/logto-io/logto/commit/416f4e86e390318dbb0bdb262139ca4ec72ce5fe))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **core:** update `koaAuth()` to inject detailed auth info ([#1977](https://github.com/logto-io/logto/issues/1977)) ([d4fc7b3](https://github.com/logto-io/logto/commit/d4fc7b3e5f4979f8419b87393bfd1af02e9a191d))
|
||||
* **core:** update user scopes ([#1922](https://github.com/logto-io/logto/issues/1922)) ([8d22b5c](https://github.com/logto-io/logto/commit/8d22b5c468e5148a3815abf93de14644cdf68e8e))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.9](https://github.com/logto-io/logto/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-09-07)
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { merge, Config } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
testPathIgnorePatterns: ['/core/connectors/'],
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.ts'],
|
||||
globalSetup: './jest.global-setup.ts',
|
||||
});
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Generate private key for tests
|
||||
*/
|
||||
import { generateKeyPairSync } from 'crypto';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
export const privateKeyPath = 'oidc-private-key.test.pem';
|
||||
|
||||
const globalSetup = () => {
|
||||
const { privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
|
||||
writeFileSync(privateKeyPath, privateKey);
|
||||
};
|
||||
|
||||
export default globalSetup;
|
|
@ -4,14 +4,10 @@
|
|||
|
||||
import envSet from '@/env-set';
|
||||
|
||||
import { privateKeyPath } from './jest.global-setup';
|
||||
jest.mock('@/lib/logto-config');
|
||||
jest.mock('@/env-set/check-alteration-state');
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
(async () => {
|
||||
process.env = {
|
||||
...process.env,
|
||||
OIDC_PRIVATE_KEY_PATHS: privateKeyPath,
|
||||
OIDC_COOKIE_KEYS: '["LOGTOSEKRIT1"]',
|
||||
};
|
||||
await envSet.load();
|
||||
})();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@logto/core",
|
||||
"version": "1.0.0-beta.9",
|
||||
"version": "1.0.0-beta.10",
|
||||
"description": "The open source identity solution.",
|
||||
"main": "build/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
|
@ -16,18 +16,20 @@
|
|||
"start": "NODE_ENV=production node build/index.js",
|
||||
"add-connector": "node build/cli/add-connector.js",
|
||||
"add-official-connectors": "node build/cli/add-official-connectors.js",
|
||||
"alteration": "node build/cli/alteration.js",
|
||||
"alteration": "logto db alt",
|
||||
"cli": "logto",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"test:ci": "jest --coverage --silent",
|
||||
"test:report": "codecov -F core"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/cli": "^1.0.0-beta.10",
|
||||
"@logto/connector-kit": "^1.0.0-beta.13",
|
||||
"@logto/core-kit": "^1.0.0-beta.16",
|
||||
"@logto/language-kit": "^1.0.0-beta.16",
|
||||
"@logto/phrases": "^1.0.0-beta.9",
|
||||
"@logto/phrases-ui": "^1.0.0-beta.9",
|
||||
"@logto/schemas": "^1.0.0-beta.9",
|
||||
"@logto/phrases": "^1.0.0-beta.10",
|
||||
"@logto/phrases-ui": "^1.0.0-beta.10",
|
||||
"@logto/schemas": "^1.0.0-beta.10",
|
||||
"@silverhand/essentials": "^1.2.1",
|
||||
"chalk": "^4",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
@ -37,6 +39,7 @@
|
|||
"deepmerge": "^4.2.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"etag": "^1.8.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"got": "^11.8.2",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"i18next": "^21.8.16",
|
||||
|
@ -75,6 +78,7 @@
|
|||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/etag": "^1.8.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/http-errors": "^1.8.2",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^28.1.6",
|
||||
|
|
|
@ -17,6 +17,7 @@ export const mockUser: User = {
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_789,
|
||||
createdAt: 1_650_969_000_000,
|
||||
};
|
||||
|
||||
export const mockUserResponse = pick(mockUser, ...userInfoSelectFields);
|
||||
|
@ -38,6 +39,7 @@ export const mockUserWithPassword: User = {
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_789,
|
||||
createdAt: 1_650_969_000_000,
|
||||
};
|
||||
|
||||
export const mockUserList: User[] = [
|
||||
|
@ -55,6 +57,7 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
@ -70,6 +73,7 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
|
@ -85,6 +89,7 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
|
@ -100,6 +105,7 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
|
@ -115,6 +121,7 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export const alterationStateKey = 'alterationState';
|
||||
export const logtoConfigsTableFilePath = 'node_modules/@logto/schemas/tables/logto_configs.sql';
|
||||
export const alterationFilesDirectory = 'node_modules/@logto/schemas/alterations';
|
|
@ -1,190 +0,0 @@
|
|||
import { LogtoConfigs } from '@logto/schemas';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { convertToIdentifiers } from '@/database/utils';
|
||||
import { QueryType, expectSqlAssert } from '@/utils/test-utils';
|
||||
|
||||
import * as functions from '.';
|
||||
import { alterationStateKey } from './constants';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
const {
|
||||
createLogtoConfigsTable,
|
||||
isLogtoConfigsTableExists,
|
||||
updateDatabaseTimestamp,
|
||||
getCurrentDatabaseTimestamp,
|
||||
getUndeployedAlterations,
|
||||
} = functions;
|
||||
const pool = createMockPool({
|
||||
query: async (sql, values) => {
|
||||
return mockQuery(sql, values);
|
||||
},
|
||||
});
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
const timestamp = 1_663_923_776;
|
||||
|
||||
describe('isLogtoConfigsTableExists()', () => {
|
||||
it('generates "select exists" sql and query for result', async () => {
|
||||
const expectSql = sql`
|
||||
select exists (
|
||||
select from
|
||||
pg_tables
|
||||
where
|
||||
tablename = $1
|
||||
);
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([LogtoConfigs.table]);
|
||||
|
||||
return createMockQueryResult([{ exists: true }]);
|
||||
});
|
||||
|
||||
await expect(isLogtoConfigsTableExists(pool)).resolves.toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentDatabaseTimestamp()', () => {
|
||||
it('returns null if query failed (table not found)', async () => {
|
||||
mockQuery.mockRejectedValueOnce(new Error('table not found'));
|
||||
|
||||
await expect(getCurrentDatabaseTimestamp(pool)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if the row is not found', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([alterationStateKey]);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseTimestamp(pool)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if the value is in bad format', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([alterationStateKey]);
|
||||
|
||||
return createMockQueryResult([{ value: 'some_value' }]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseTimestamp(pool)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('returns the timestamp from database', async () => {
|
||||
const expectSql = sql`
|
||||
select * from ${table} where ${fields.key}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([alterationStateKey]);
|
||||
|
||||
// @ts-expect-error createMockQueryResult doesn't support jsonb
|
||||
return createMockQueryResult([{ value: { timestamp, updatedAt: 'now' } }]);
|
||||
});
|
||||
|
||||
await expect(getCurrentDatabaseTimestamp(pool)).resolves.toEqual(timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLogtoConfigsTable()', () => {
|
||||
it('sends sql to create target table', async () => {
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expect(sql).toContain(LogtoConfigs.table);
|
||||
expect(sql).toContain('create table');
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
await createLogtoConfigsTable(pool);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDatabaseTimestamp()', () => {
|
||||
const expectSql = sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values ($1, $2)
|
||||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`;
|
||||
const updatedAt = '2022-09-21T06:32:46.583Z';
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(updatedAt));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('calls createLogtoConfigsTable() if table does not exist', async () => {
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
const mockCreateLogtoConfigsTable = jest
|
||||
.spyOn(functions, 'createLogtoConfigsTable')
|
||||
.mockImplementationOnce(jest.fn());
|
||||
jest.spyOn(functions, 'isLogtoConfigsTableExists').mockResolvedValueOnce(false);
|
||||
|
||||
await updateDatabaseTimestamp(pool, timestamp);
|
||||
expect(mockCreateLogtoConfigsTable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends upsert sql with timestamp and updatedAt', async () => {
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([alterationStateKey, JSON.stringify({ timestamp, updatedAt })]);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
jest.spyOn(functions, 'isLogtoConfigsTableExists').mockResolvedValueOnce(true);
|
||||
|
||||
await updateDatabaseTimestamp(pool, timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUndeployedAlterations()', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(functions, 'getAlterationFiles')
|
||||
.mockResolvedValueOnce([
|
||||
'1.0.0-1663923770-a.js',
|
||||
'1.0.0-1663923772-c.js',
|
||||
'1.0.0-1663923771-b.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns all files with right order if database timestamp is null', async () => {
|
||||
jest.spyOn(functions, 'getCurrentDatabaseTimestamp').mockResolvedValueOnce(null);
|
||||
|
||||
await expect(getUndeployedAlterations(pool)).resolves.toEqual([
|
||||
'1.0.0-1663923770-a.js',
|
||||
'1.0.0-1663923771-b.js',
|
||||
'1.0.0-1663923772-c.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns files whose timestamp is greater then database timstamp', async () => {
|
||||
jest.spyOn(functions, 'getCurrentDatabaseTimestamp').mockResolvedValueOnce(1_663_923_770);
|
||||
|
||||
await expect(getUndeployedAlterations(pool)).resolves.toEqual([
|
||||
'1.0.0-1663923771-b.js',
|
||||
'1.0.0-1663923772-c.js',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,157 +0,0 @@
|
|||
import { existsSync } from 'fs';
|
||||
import { readdir, readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { LogtoConfig, LogtoConfigs } from '@logto/schemas';
|
||||
import {
|
||||
AlterationScript,
|
||||
AlterationState,
|
||||
alterationStateGuard,
|
||||
} from '@logto/schemas/lib/types/alteration';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import { DatabasePool, sql } from 'slonik';
|
||||
import { raw } from 'slonik-sql-tag-raw';
|
||||
|
||||
import { convertToIdentifiers } from '@/database/utils';
|
||||
|
||||
import {
|
||||
logtoConfigsTableFilePath,
|
||||
alterationStateKey,
|
||||
alterationFilesDirectory,
|
||||
} from './constants';
|
||||
import { getTimestampFromFileName, alterationFileNameRegex } from './utils';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
|
||||
export const isLogtoConfigsTableExists = async (pool: DatabasePool) => {
|
||||
const { exists } = await pool.one<{ exists: boolean }>(sql`
|
||||
select exists (
|
||||
select from
|
||||
pg_tables
|
||||
where
|
||||
tablename = ${LogtoConfigs.table}
|
||||
);
|
||||
`);
|
||||
|
||||
return exists;
|
||||
};
|
||||
|
||||
export const getCurrentDatabaseTimestamp = async (pool: DatabasePool) => {
|
||||
try {
|
||||
const query = await pool.maybeOne<LogtoConfig>(
|
||||
sql`select * from ${table} where ${fields.key}=${alterationStateKey}`
|
||||
);
|
||||
const { timestamp } = alterationStateGuard.parse(query?.value);
|
||||
|
||||
return timestamp;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const createLogtoConfigsTable = async (pool: DatabasePool) => {
|
||||
const tableQuery = await readFile(logtoConfigsTableFilePath, 'utf8');
|
||||
await pool.query(sql`${raw(tableQuery)}`);
|
||||
};
|
||||
|
||||
export const updateDatabaseTimestamp = async (pool: DatabasePool, timestamp?: number) => {
|
||||
if (!(await isLogtoConfigsTableExists(pool))) {
|
||||
await createLogtoConfigsTable(pool);
|
||||
}
|
||||
|
||||
const value: AlterationState = {
|
||||
timestamp: timestamp ?? (await getLatestAlterationTimestamp()),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await pool.query(
|
||||
sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values (${alterationStateKey}, ${JSON.stringify(value)})
|
||||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
export const getLatestAlterationTimestamp = async () => {
|
||||
const files = await getAlterationFiles();
|
||||
|
||||
const latestFile = files[files.length - 1];
|
||||
|
||||
if (!latestFile) {
|
||||
throw new Error('No alteration files found.');
|
||||
}
|
||||
|
||||
return getTimestampFromFileName(latestFile);
|
||||
};
|
||||
|
||||
export const getAlterationFiles = async () => {
|
||||
if (!existsSync(alterationFilesDirectory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directory = await readdir(alterationFilesDirectory);
|
||||
const files = directory.filter((file) => alterationFileNameRegex.test(file));
|
||||
|
||||
return files
|
||||
.slice()
|
||||
.sort((file1, file2) => getTimestampFromFileName(file1) - getTimestampFromFileName(file2));
|
||||
};
|
||||
|
||||
export const getUndeployedAlterations = async (pool: DatabasePool) => {
|
||||
const databaseTimestamp = await getCurrentDatabaseTimestamp(pool);
|
||||
const files = await getAlterationFiles();
|
||||
|
||||
return files
|
||||
.filter((file) => !databaseTimestamp || getTimestampFromFileName(file) > databaseTimestamp)
|
||||
.slice()
|
||||
.sort((file1, file2) => getTimestampFromFileName(file1) - getTimestampFromFileName(file2));
|
||||
};
|
||||
|
||||
const importAlteration = async (file: string): Promise<AlterationScript> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(
|
||||
path.join(alterationFilesDirectory, file).replace('node_modules/', '')
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return module.default as AlterationScript;
|
||||
};
|
||||
|
||||
const deployAlteration = async (pool: DatabasePool, file: string) => {
|
||||
const { up } = await importAlteration(file);
|
||||
|
||||
try {
|
||||
await pool.transaction(async (connect) => {
|
||||
await up(connect);
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.log(`${chalk.red('[alteration]')} run ${file} failed: ${error.message}.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await updateDatabaseTimestamp(pool, getTimestampFromFileName(file));
|
||||
console.log(`${chalk.blue('[alteration]')} run ${file} succeeded.`);
|
||||
};
|
||||
|
||||
export const deployAlterations = async (pool: DatabasePool) => {
|
||||
const alterations = await getUndeployedAlterations(pool);
|
||||
|
||||
console.log(
|
||||
`${chalk.blue('[alteration]')} found ${alterations.length} alteration${conditionalString(
|
||||
alterations.length > 1 && 's'
|
||||
)}`
|
||||
);
|
||||
|
||||
// The await inside the loop is intended, alterations should run in order
|
||||
for (const alteration of alterations) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deployAlteration(pool, alteration);
|
||||
}
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
import { getTimestampFromFileName } from './utils';
|
||||
|
||||
describe('getTimestampFromFileName()', () => {
|
||||
it('should get for 1.0.0-1663923211.js', () => {
|
||||
expect(getTimestampFromFileName('1.0.0-1663923211.js')).toEqual(1_663_923_211);
|
||||
});
|
||||
|
||||
it('should get for 1.0.0-1663923211-user-table.js', () => {
|
||||
expect(getTimestampFromFileName('1.0.0-1663923211-user-table.js')).toEqual(1_663_923_211);
|
||||
});
|
||||
|
||||
it('should throw for 166392321.js', () => {
|
||||
expect(() => getTimestampFromFileName('166392321.js')).toThrowError();
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
export const alterationFileNameRegex = /-(\d{10,11})-?.*\.js$/;
|
||||
|
||||
export const getTimestampFromFileName = (fileName: string) => {
|
||||
const match = alterationFileNameRegex.exec(fileName);
|
||||
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Can not get timestamp: ${fileName}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
import 'module-alias/register';
|
||||
import { assertEnv } from '@silverhand/essentials';
|
||||
import { createPool } from 'slonik';
|
||||
|
||||
import { deployAlterations } from '@/alteration';
|
||||
import { configDotEnv } from '@/env-set/dot-env';
|
||||
|
||||
configDotEnv();
|
||||
|
||||
const deploy = async () => {
|
||||
const databaseUrl = assertEnv('DB_URL');
|
||||
const pool = await createPool(databaseUrl);
|
||||
await deployAlterations(pool);
|
||||
await pool.end();
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command !== 'deploy') {
|
||||
throw new Error('Unsupported command.');
|
||||
}
|
||||
|
||||
void deploy();
|
|
@ -1,100 +0,0 @@
|
|||
import { readdir, readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { SchemaLike, seeds } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import decamelize from 'decamelize';
|
||||
import { createPool, parseDsn, sql, stringifyDsn } from 'slonik';
|
||||
import { createInterceptors } from 'slonik-interceptor-preset';
|
||||
import { raw } from 'slonik-sql-tag-raw';
|
||||
|
||||
import { updateDatabaseTimestamp } from '@/alteration';
|
||||
import { buildApplicationSecret } from '@/utils/id';
|
||||
|
||||
import { convertToPrimitiveOrSql } from './utils';
|
||||
|
||||
const {
|
||||
managementResource,
|
||||
defaultSignInExperience,
|
||||
createDefaultSetting,
|
||||
createDemoAppApplication,
|
||||
defaultRole,
|
||||
} = seeds;
|
||||
const tableDirectory = 'node_modules/@logto/schemas/tables';
|
||||
|
||||
export const replaceDsnDatabase = (dsn: string, databaseName: string): string =>
|
||||
stringifyDsn({ ...parseDsn(dsn), databaseName });
|
||||
|
||||
/**
|
||||
* Create a database.
|
||||
* @returns DSN with the created database name.
|
||||
*/
|
||||
export const createDatabase = async (dsn: string, databaseName: string): Promise<string> => {
|
||||
const pool = await createPool(replaceDsnDatabase(dsn, 'postgres'));
|
||||
|
||||
await pool.query(sql`
|
||||
create database ${sql.identifier([databaseName])}
|
||||
with
|
||||
encoding = 'UTF8'
|
||||
connection_limit = -1;
|
||||
`);
|
||||
await pool.end();
|
||||
|
||||
console.log(`${chalk.blue('[create]')} Database ${databaseName} successfully created.`);
|
||||
|
||||
return replaceDsnDatabase(dsn, databaseName);
|
||||
};
|
||||
|
||||
export const insertInto = <T extends SchemaLike>(object: T, table: string) => {
|
||||
const keys = Object.keys(object);
|
||||
|
||||
return sql`
|
||||
insert into ${sql.identifier([table])}
|
||||
(${sql.join(
|
||||
keys.map((key) => sql.identifier([decamelize(key)])),
|
||||
sql`, `
|
||||
)})
|
||||
values (${sql.join(
|
||||
keys.map((key) => convertToPrimitiveOrSql(key, object[key] ?? null)),
|
||||
sql`, `
|
||||
)})
|
||||
`;
|
||||
};
|
||||
|
||||
export const createDatabaseCli = async (dsn: string) => {
|
||||
const pool = await createPool(dsn, { interceptors: createInterceptors() });
|
||||
|
||||
const createTables = async () => {
|
||||
const directory = await readdir(tableDirectory);
|
||||
const tableFiles = directory.filter((file) => file.endsWith('.sql'));
|
||||
const queries = await Promise.all(
|
||||
tableFiles.map<Promise<[string, string]>>(async (file) => [
|
||||
file,
|
||||
await readFile(path.join(tableDirectory, file), 'utf8'),
|
||||
])
|
||||
);
|
||||
|
||||
// Await in loop is intended for better error handling
|
||||
for (const [file, query] of queries) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await pool.query(sql`${raw(query)}`);
|
||||
console.log(`${chalk.blue('[create-tables]')} Run ${file} succeeded.`);
|
||||
}
|
||||
|
||||
await updateDatabaseTimestamp(pool);
|
||||
console.log(`${chalk.blue('[create-tables]')} Update alteration state succeeded.`);
|
||||
};
|
||||
|
||||
const seedTables = async () => {
|
||||
await Promise.all([
|
||||
pool.query(insertInto(managementResource, 'resources')),
|
||||
pool.query(insertInto(createDefaultSetting(), 'settings')),
|
||||
pool.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||
pool.query(insertInto(createDemoAppApplication(buildApplicationSecret()), 'applications')),
|
||||
pool.query(insertInto(defaultRole, 'roles')),
|
||||
]);
|
||||
console.log(`${chalk.blue('[seed-tables]')} Seed tables succeeded.`);
|
||||
};
|
||||
|
||||
return { createTables, seedTables, pool };
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export const checkAlterationState = async () => {};
|
24
packages/core/src/env-set/check-alteration-state.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { getUndeployedAlterations } from '@logto/cli/lib/commands/database/alteration';
|
||||
import chalk from 'chalk';
|
||||
import { DatabasePool } from 'slonik';
|
||||
|
||||
export const checkAlterationState = async (pool: DatabasePool) => {
|
||||
const alterations = await getUndeployedAlterations(pool);
|
||||
|
||||
if (alterations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`${chalk.red(
|
||||
'[error]'
|
||||
)} Found undeployed database alterations, you must deploy them first by ${chalk.green(
|
||||
'npm run alteration deploy'
|
||||
)} command.\n\n` +
|
||||
` See ${chalk.blue(
|
||||
'https://docs.logto.io/docs/recipes/deployment/#database-alteration'
|
||||
)} for reference.\n`
|
||||
);
|
||||
|
||||
throw new Error(`Undeployed database alterations found.`);
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { DatabasePool } from 'slonik';
|
||||
|
||||
import { getUndeployedAlterations, deployAlterations } from '@/alteration';
|
||||
|
||||
import { allYes } from './parameters';
|
||||
|
||||
export const checkAlterationState = async (pool: DatabasePool) => {
|
||||
const alterations = await getUndeployedAlterations(pool);
|
||||
|
||||
if (alterations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const error = new Error(
|
||||
`Found undeployed database alterations, you must deploy them first by "pnpm alteration deploy" command, reference: https://docs.logto.io/docs/recipes/deployment/#database-alteration`
|
||||
);
|
||||
|
||||
if (allYes) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const deploy = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message: `Found undeployed alterations, would you like to deploy now?`,
|
||||
});
|
||||
|
||||
if (!deploy.value) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await deployAlterations(pool);
|
||||
};
|
|
@ -1,87 +1,12 @@
|
|||
import { assertEnv, conditional, getEnv, Optional } from '@silverhand/essentials';
|
||||
import inquirer from 'inquirer';
|
||||
import { createPool } from 'slonik';
|
||||
import { assertEnv } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import { createMockPool, createMockQueryResult, createPool } from 'slonik';
|
||||
import { createInterceptors } from 'slonik-interceptor-preset';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createDatabase, createDatabaseCli, replaceDsnDatabase } from '@/database/seed';
|
||||
|
||||
import { appendDotEnv } from './dot-env';
|
||||
import { allYes, noInquiry } from './parameters';
|
||||
|
||||
const defaultDatabaseUrl = getEnv('DB_URL_DEFAULT', 'postgres://@localhost:5432');
|
||||
const defaultDatabaseName = 'logto';
|
||||
|
||||
const initDatabase = async (dsn: string): Promise<[string, boolean]> => {
|
||||
try {
|
||||
return [await createDatabase(dsn, defaultDatabaseName), true];
|
||||
} catch (error: unknown) {
|
||||
const result = z.object({ code: z.string() }).safeParse(error);
|
||||
|
||||
// https://www.postgresql.org/docs/12/errcodes-appendix.html
|
||||
const databaseExists = result.success && result.data.code === '42P04';
|
||||
|
||||
if (!databaseExists) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (allYes) {
|
||||
return [replaceDsnDatabase(dsn, defaultDatabaseName), false];
|
||||
}
|
||||
|
||||
const useCurrent = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message: `A database named "${defaultDatabaseName}" already exists. Would you like to use it without filling the initial data?`,
|
||||
});
|
||||
|
||||
if (useCurrent.value) {
|
||||
return [replaceDsnDatabase(dsn, defaultDatabaseName), false];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const inquireForLogtoDsn = async (key: string): Promise<[Optional<string>, boolean]> => {
|
||||
if (allYes) {
|
||||
return initDatabase(defaultDatabaseUrl);
|
||||
}
|
||||
|
||||
const setUp = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message: `No Postgres DSN (${key}) found in env variables. Would you like to set up a new Logto database?`,
|
||||
});
|
||||
|
||||
if (!setUp.value) {
|
||||
const dsn = await inquirer.prompt({
|
||||
name: 'value',
|
||||
default: new URL(defaultDatabaseName, defaultDatabaseUrl).href,
|
||||
message: 'Please input the DSN which points to an existing Logto database:',
|
||||
});
|
||||
|
||||
return [conditional<string>(dsn.value && String(dsn.value)), false];
|
||||
}
|
||||
|
||||
const dsnAnswer = await inquirer.prompt({
|
||||
name: 'value',
|
||||
default: new URL(defaultDatabaseUrl).href,
|
||||
message: `Please input the DSN _WITHOUT_ database name:`,
|
||||
});
|
||||
const dsn = conditional<string>(dsnAnswer.value && String(dsnAnswer.value));
|
||||
|
||||
if (!dsn) {
|
||||
return [dsn, false];
|
||||
}
|
||||
|
||||
return initDatabase(dsn);
|
||||
};
|
||||
|
||||
const createPoolByEnv = async (isTest: boolean) => {
|
||||
// Database connection is disabled in unit test environment
|
||||
if (isTest) {
|
||||
return;
|
||||
return createMockPool({ query: async () => createMockQueryResult([]) });
|
||||
}
|
||||
|
||||
const key = 'DB_URL';
|
||||
|
@ -92,26 +17,24 @@ const createPoolByEnv = async (isTest: boolean) => {
|
|||
|
||||
return await createPool(databaseDsn, { interceptors });
|
||||
} catch (error: unknown) {
|
||||
if (noInquiry) {
|
||||
throw error;
|
||||
if (error instanceof Error && error.message === `env variable ${key} not found`) {
|
||||
console.error(
|
||||
`${chalk.red('[error]')} No Postgres DSN (${chalk.green(
|
||||
key
|
||||
)}) found in env variables.\n\n` +
|
||||
` Either provide it in your env, or add it to the ${chalk.blue(
|
||||
'.env'
|
||||
)} file in the Logto project root.\n\n` +
|
||||
` If you want to set up a new Logto database, run ${chalk.green(
|
||||
'npm run cli db seed'
|
||||
)} before setting env ${chalk.green(key)}.\n\n` +
|
||||
` Visit ${chalk.blue(
|
||||
'https://docs.logto.io/docs/references/core/configuration'
|
||||
)} for more info about setting up env.\n`
|
||||
);
|
||||
}
|
||||
|
||||
const [dsn, needsSeed] = await inquireForLogtoDsn(key);
|
||||
|
||||
if (!dsn) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const cli = await createDatabaseCli(dsn);
|
||||
|
||||
if (needsSeed) {
|
||||
await cli.createTables();
|
||||
await cli.seedTables();
|
||||
}
|
||||
|
||||
appendDotEnv(key, dsn);
|
||||
|
||||
return cli.pool;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ import path from 'path';
|
|||
import { getEnv, getEnvAsStringArray, Optional } from '@silverhand/essentials';
|
||||
import { DatabasePool } from 'slonik';
|
||||
|
||||
import { getOidcConfigs } from '@/lib/logto-config';
|
||||
import { appendPath } from '@/utils/url';
|
||||
|
||||
import { addConnectors } from './add-connectors';
|
||||
import { checkAlterationState } from './check-migration-state';
|
||||
import { checkAlterationState } from './check-alteration-state';
|
||||
import createPoolByEnv from './create-pool-by-env';
|
||||
import loadOidcValues from './oidc';
|
||||
import { isTrue } from './parameters';
|
||||
|
@ -44,7 +45,6 @@ const loadEnvValues = async () => {
|
|||
userDefaultRoleNames: getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES'),
|
||||
developmentUserId: getEnv('DEVELOPMENT_USER_ID'),
|
||||
trustProxyHeader: isTrue(getEnv('TRUST_PROXY_HEADER')),
|
||||
oidc: await loadOidcValues(appendPath(endpoint, '/oidc').toString()),
|
||||
adminConsoleUrl: appendPath(endpoint, '/console'),
|
||||
connectorDirectory: getEnv('CONNECTOR_DIRECTORY', defaultConnectorDirectory),
|
||||
});
|
||||
|
@ -60,6 +60,7 @@ const throwNotLoadedError = () => {
|
|||
function createEnvSet() {
|
||||
let values: Optional<Awaited<ReturnType<typeof loadEnvValues>>>;
|
||||
let pool: Optional<DatabasePool>;
|
||||
let oidc: Optional<Awaited<ReturnType<typeof loadOidcValues>>>;
|
||||
|
||||
return {
|
||||
get values() {
|
||||
|
@ -76,15 +77,24 @@ function createEnvSet() {
|
|||
|
||||
return pool;
|
||||
},
|
||||
get poolSafe() {
|
||||
return pool;
|
||||
},
|
||||
get oidc() {
|
||||
if (!oidc) {
|
||||
return throwNotLoadedError();
|
||||
}
|
||||
|
||||
return oidc;
|
||||
},
|
||||
load: async () => {
|
||||
values = await loadEnvValues();
|
||||
pool = await createPoolByEnv(values.isTest);
|
||||
await addConnectors(values.connectorDirectory);
|
||||
|
||||
if (pool) {
|
||||
await checkAlterationState(pool);
|
||||
}
|
||||
const [, oidcConfigs] = await Promise.all([checkAlterationState(pool), getOidcConfigs(pool)]);
|
||||
oidc = await loadOidcValues(appendPath(values.endpoint, '/oidc').toString(), oidcConfigs);
|
||||
|
||||
await addConnectors(values.connectorDirectory);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
import crypto from 'crypto';
|
||||
import fs, { PathOrFileDescriptor } from 'fs';
|
||||
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
import { readCookieKeys, readPrivateKeys } from './oidc';
|
||||
|
||||
describe('oidc env-set', () => {
|
||||
const envBackup = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...envBackup };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should read OIDC private keys if raw `OIDC_PRIVATE_KEYS` is provided', async () => {
|
||||
const rawKeys = [
|
||||
'-----BEGIN PRIVATE KEY-----\nFOO\n-----END PRIVATE KEY-----',
|
||||
'-----BEGIN PRIVATE KEY-----\nBAR\n-----END PRIVATE KEY-----',
|
||||
];
|
||||
process.env.OIDC_PRIVATE_KEYS = rawKeys.join(',');
|
||||
|
||||
const privateKeys = await readPrivateKeys();
|
||||
|
||||
expect(privateKeys).toEqual([
|
||||
'-----BEGIN PRIVATE KEY-----\nFOO\n-----END PRIVATE KEY-----',
|
||||
'-----BEGIN PRIVATE KEY-----\nBAR\n-----END PRIVATE KEY-----',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transpile and read OIDC private keys if base64-formatted `OIDC_PRIVATE_KEYS` is provided', async () => {
|
||||
const base64Keys = ['foo', 'bar'].map((key) => Buffer.from(key, 'utf8').toString('base64'));
|
||||
process.env.OIDC_PRIVATE_KEYS = base64Keys.join(',');
|
||||
|
||||
const privateKeys = await readPrivateKeys();
|
||||
|
||||
expect(privateKeys).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should read OIDC private keys if `OIDC_PRIVATE_KEY_PATHS` is provided', async () => {
|
||||
process.env.OIDC_PRIVATE_KEY_PATHS = 'foo.pem, bar.pem';
|
||||
const existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
const readFileSyncSpy = jest
|
||||
.spyOn(fs, 'readFileSync')
|
||||
.mockImplementation((path: PathOrFileDescriptor) => path.toString());
|
||||
|
||||
const privateKeys = await readPrivateKeys();
|
||||
|
||||
expect(privateKeys).toEqual(['foo.pem', 'bar.pem']);
|
||||
|
||||
existsSyncSpy.mockRestore();
|
||||
readFileSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should generate a default OIDC private key if neither `OIDC_PRIVATE_KEY_PATHS` nor `OIDC_PRIVATE_KEYS` is provided', async () => {
|
||||
process.env.OIDC_PRIVATE_KEYS = '';
|
||||
process.env.OIDC_PRIVATE_KEY_PATHS = '';
|
||||
|
||||
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
||||
throw new Error('Dummy read file error');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
|
||||
const promptMock = jest.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true });
|
||||
|
||||
const generateKeyPairSyncSpy = jest.spyOn(crypto, 'generateKeyPairSync');
|
||||
|
||||
const privateKeys = await readPrivateKeys();
|
||||
|
||||
expect(privateKeys.length).toBe(1);
|
||||
expect(generateKeyPairSyncSpy).toHaveBeenCalled();
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled();
|
||||
|
||||
readFileSyncSpy.mockRestore();
|
||||
promptMock.mockRestore();
|
||||
generateKeyPairSyncSpy.mockRestore();
|
||||
writeFileSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw if private keys configured in `OIDC_PRIVATE_KEY_PATHS` are not found', async () => {
|
||||
process.env.OIDC_PRIVATE_KEY_PATHS = 'foo.pem, bar.pem, baz.pem';
|
||||
const existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(false);
|
||||
|
||||
await expect(readPrivateKeys()).rejects.toMatchError(
|
||||
new Error(
|
||||
`Private keys foo.pem, bar.pem, and baz.pem configured in env \`OIDC_PRIVATE_KEY_PATHS\` not found.`
|
||||
)
|
||||
);
|
||||
|
||||
existsSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should read OIDC cookie keys if `OIDC_COOKIE_KEYS` is provided', async () => {
|
||||
process.env.OIDC_COOKIE_KEYS = 'foo, bar';
|
||||
|
||||
const cookieKeys = await readCookieKeys();
|
||||
|
||||
expect(cookieKeys).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should generate a default OIDC cookie key if `OIDC_COOKIE_KEYS` is not provided', async () => {
|
||||
process.env.OIDC_COOKIE_KEYS = '';
|
||||
|
||||
const promptMock = jest.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const appendFileSyncSpy = jest.spyOn(fs, 'appendFileSync').mockImplementation(() => {});
|
||||
|
||||
const cookieKeys = await readCookieKeys();
|
||||
|
||||
expect(cookieKeys.length).toBe(1);
|
||||
expect(appendFileSyncSpy).toHaveBeenCalled();
|
||||
|
||||
promptMock.mockRestore();
|
||||
appendFileSyncSpy.mockRestore();
|
||||
});
|
||||
});
|
|
@ -1,168 +1,27 @@
|
|||
import crypto, { generateKeyPairSync } from 'crypto';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
||||
import inquirer from 'inquirer';
|
||||
import { LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas';
|
||||
import { createLocalJWKSet } from 'jose';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { exportJWK } from '@/utils/jwks';
|
||||
|
||||
import { appendDotEnv } from './dot-env';
|
||||
import { allYes, noInquiry } from './parameters';
|
||||
|
||||
const defaultLogtoOidcPrivateKeyPath = './oidc-private-key.pem';
|
||||
|
||||
const listFormatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
|
||||
|
||||
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');
|
||||
|
||||
/**
|
||||
* Try to read private keys with the following order:
|
||||
*
|
||||
* 1. From `process.env.OIDC_PRIVATE_KEYS`.
|
||||
* 2. Fetch path from `process.env.OIDC_PRIVATE_KEY_PATHS` then read from that path.
|
||||
*
|
||||
* If none of above succeed, then inquire user to generate a new key if no `--no-inquiry` presents in argv.
|
||||
*
|
||||
* @returns The private keys for OIDC provider.
|
||||
* @throws An error when failed to read a private key.
|
||||
*/
|
||||
export const readPrivateKeys = async (): Promise<string[]> => {
|
||||
const privateKeys = getEnvAsStringArray('OIDC_PRIVATE_KEYS');
|
||||
|
||||
if (privateKeys.length > 0) {
|
||||
return privateKeys.map((key) => {
|
||||
if (isBase64FormatPrivateKey(key)) {
|
||||
return Buffer.from(key, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
return key;
|
||||
});
|
||||
}
|
||||
|
||||
const privateKeyPaths = getEnvAsStringArray('OIDC_PRIVATE_KEY_PATHS');
|
||||
|
||||
/**
|
||||
* If neither `OIDC_PRIVATE_KEYS` nor `OIDC_PRIVATE_KEY_PATHS` is provided:
|
||||
*
|
||||
* 1. Try to read the private key from `defaultLogtoOidcPrivateKeyPath`
|
||||
* 2. If the `defaultLogtoOidcPrivateKeyPath` doesn't exist, then ask user to generate a new key.
|
||||
*/
|
||||
if (privateKeyPaths.length === 0) {
|
||||
try {
|
||||
return [readFileSync(defaultLogtoOidcPrivateKeyPath, 'utf8')];
|
||||
} catch (error: unknown) {
|
||||
if (noInquiry) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!allYes) {
|
||||
const answer = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `No private key found in env \`OIDC_PRIVATE_KEYS\` nor \`${defaultLogtoOidcPrivateKeyPath}\`, would you like to generate a new one?`,
|
||||
});
|
||||
|
||||
if (!answer.confirm) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
writeFileSync(defaultLogtoOidcPrivateKeyPath, privateKey);
|
||||
|
||||
return [privateKey];
|
||||
}
|
||||
}
|
||||
|
||||
const nonExistentPrivateKeys = privateKeyPaths.filter((path): boolean => !existsSync(path));
|
||||
|
||||
if (nonExistentPrivateKeys.length > 0) {
|
||||
throw new Error(
|
||||
`Private keys ${listFormatter.format(
|
||||
nonExistentPrivateKeys
|
||||
)} configured in env \`OIDC_PRIVATE_KEY_PATHS\` not found.`
|
||||
);
|
||||
}
|
||||
|
||||
return privateKeyPaths.map((path): string => readFileSync(path, 'utf8'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to read the [signing cookie keys](https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#cookieskeys) from env.
|
||||
*
|
||||
* If failed, then inquire user to generate a new keys array if no `--no-inquiry` presents in argv.
|
||||
*
|
||||
* @returns The cookie keys in array.
|
||||
*/
|
||||
export const readCookieKeys = async (): Promise<string[]> => {
|
||||
const envKey = 'OIDC_COOKIE_KEYS';
|
||||
const keys = getEnvAsStringArray(envKey);
|
||||
|
||||
if (keys.length > 0) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
const cookieKeysMissingError = new Error(
|
||||
`The OIDC cookie keys are not found. Please check the value of env \`${envKey}\`.`
|
||||
const loadOidcValues = async (issuer: string, configs: LogtoOidcConfigType) => {
|
||||
const cookieKeys = configs[LogtoOidcConfigKey.CookieKeys];
|
||||
const privateKeys = configs[LogtoOidcConfigKey.PrivateKeys].map((key) =>
|
||||
crypto.createPrivateKey(key)
|
||||
);
|
||||
|
||||
if (noInquiry) {
|
||||
throw cookieKeysMissingError;
|
||||
}
|
||||
|
||||
if (!allYes) {
|
||||
const answer = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `No cookie keys array found in env \`${envKey}\`, would you like to generate a new one?`,
|
||||
});
|
||||
|
||||
if (!answer.confirm) {
|
||||
throw cookieKeysMissingError;
|
||||
}
|
||||
}
|
||||
|
||||
const generated = nanoid();
|
||||
appendDotEnv(envKey, generated);
|
||||
|
||||
return [generated];
|
||||
};
|
||||
|
||||
const loadOidcValues = async (issuer: string) => {
|
||||
const cookieKeys = await readCookieKeys();
|
||||
|
||||
const configPrivateKeys = await readPrivateKeys();
|
||||
const privateKeys = configPrivateKeys.map((key) => crypto.createPrivateKey(key));
|
||||
const publicKeys = privateKeys.map((key) => crypto.createPublicKey(key));
|
||||
const privateJwks = await Promise.all(privateKeys.map(async (key) => exportJWK(key)));
|
||||
const publicJwks = await Promise.all(publicKeys.map(async (key) => exportJWK(key)));
|
||||
const localJWKSet = createLocalJWKSet({ keys: publicJwks });
|
||||
|
||||
/**
|
||||
* This interval helps to avoid concurrency issues when exchanging the rotating refresh token multiple times within a given timeframe.
|
||||
* During the leeway window (in seconds), the consumed refresh token will be considered as valid.
|
||||
* This is useful for distributed apps and serverless apps like Next.js, in which there is no shared memory.
|
||||
*/
|
||||
const refreshTokenReuseInterval = getEnv('OIDC_REFRESH_TOKEN_REUSE_INTERVAL', '3');
|
||||
const refreshTokenReuseInterval = configs[LogtoOidcConfigKey.RefreshTokenReuseInterval];
|
||||
|
||||
return Object.freeze({
|
||||
cookieKeys,
|
||||
privateJwks,
|
||||
localJWKSet,
|
||||
issuer,
|
||||
refreshTokenReuseInterval: Number(refreshTokenReuseInterval),
|
||||
refreshTokenReuseInterval,
|
||||
defaultIdTokenTtl: 60 * 60,
|
||||
defaultRefreshTokenTtl: 14 * 24 * 60 * 60,
|
||||
});
|
||||
|
|
|
@ -27,5 +27,6 @@ import initI18n from './i18n/init';
|
|||
await initApp(app);
|
||||
} catch (error: unknown) {
|
||||
console.log('Error while initializing app', error);
|
||||
await envSet.poolSafe?.end();
|
||||
}
|
||||
})();
|
||||
|
|
21
packages/core/src/lib/__mocks__/logto-config.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { generateKeyPairSync } from 'crypto';
|
||||
|
||||
import { LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas';
|
||||
|
||||
const { privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
|
||||
export const getOidcConfigs = async (): Promise<LogtoOidcConfigType> => ({
|
||||
[LogtoOidcConfigKey.PrivateKeys]: [privateKey],
|
||||
[LogtoOidcConfigKey.CookieKeys]: ['LOGTOSEKRIT1'],
|
||||
[LogtoOidcConfigKey.RefreshTokenReuseInterval]: 3,
|
||||
});
|
35
packages/core/src/lib/logto-config.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { getRowsByKeys } from '@logto/cli/lib/queries/logto-config';
|
||||
import { logtoOidcConfigGuard, LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import { DatabasePool, DatabaseTransactionConnection } from 'slonik';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
export const getOidcConfigs = async (
|
||||
pool: DatabasePool | DatabaseTransactionConnection
|
||||
): Promise<LogtoOidcConfigType> => {
|
||||
try {
|
||||
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
||||
|
||||
return z
|
||||
.object(logtoOidcConfigGuard)
|
||||
.parse(Object.fromEntries(rows.map(({ key, value }) => [key, value])));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ZodError) {
|
||||
console.error(
|
||||
error.issues
|
||||
.map(({ message, path }) => `${message} at ${chalk.green(path.join('.'))}`)
|
||||
.join('\n')
|
||||
);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`\n${chalk.red('[error]')} Failed to get OIDC configs from your Logto database.` +
|
||||
' Did you forget to seed your database?\n\n' +
|
||||
` Use ${chalk.green('npm run cli db seed')} to seed your Logto database;\n` +
|
||||
` Or use ${chalk.green('npm run cli db seed oidc')} to seed OIDC configs alone.\n`
|
||||
);
|
||||
throw new Error('Failed to get configs');
|
||||
}
|
||||
};
|
|
@ -49,7 +49,7 @@ export const verifyBearerTokenFromRequest = async (
|
|||
request: Request,
|
||||
resourceIndicator = managementResource.indicator
|
||||
): Promise<TokenInfo> => {
|
||||
const { isProduction, isIntegrationTest, developmentUserId, oidc } = envSet.values;
|
||||
const { isProduction, isIntegrationTest, developmentUserId } = envSet.values;
|
||||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && userId) {
|
||||
|
@ -57,7 +57,7 @@ export const verifyBearerTokenFromRequest = async (
|
|||
}
|
||||
|
||||
try {
|
||||
const { localJWKSet, issuer } = oidc;
|
||||
const { localJWKSet, issuer } = envSet.oidc;
|
||||
const {
|
||||
payload: { sub, client_id: clientId, role_names: roleNames },
|
||||
} = await jwtVerify(extractBearerTokenFromHeaders(request.headers), localJWKSet, {
|
||||
|
|
|
@ -17,11 +17,13 @@ type SessionPayload = {
|
|||
|
||||
type AddLogContext = (sessionPayload: SessionPayload) => void;
|
||||
|
||||
export type WithLogContext<ContextT extends IRouterParamContext = IRouterParamContext> =
|
||||
ContextT & {
|
||||
addLogContext: AddLogContext;
|
||||
log: MergeLog;
|
||||
};
|
||||
export type LogContext = {
|
||||
addLogContext: AddLogContext;
|
||||
log: MergeLog;
|
||||
};
|
||||
|
||||
export type WithLogContext<ContextT extends IRouterParamContext = IRouterParamContext> = ContextT &
|
||||
LogContext;
|
||||
|
||||
type Logger = {
|
||||
type?: LogType;
|
||||
|
|
|
@ -23,7 +23,7 @@ import { claimToUserKey, getUserClaims } from './scope';
|
|||
|
||||
export default async function initOidc(app: Koa): Promise<Provider> {
|
||||
const { issuer, cookieKeys, privateJwks, defaultIdTokenTtl, defaultRefreshTokenTtl } =
|
||||
envSet.values.oidc;
|
||||
envSet.oidc;
|
||||
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
||||
|
||||
const cookieConfig = Object.freeze({
|
||||
|
|