0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

Merge pull request #46 from logto-io/gao--add-phrases-package

feat(phrases): add package and refactor error code
This commit is contained in:
Gao Sun 2021-07-28 20:04:19 +08:00 committed by GitHub
commit ee06a61503
32 changed files with 269 additions and 120 deletions

40
.github/workflows/phrases-main.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Phrases
on:
push:
branches: [ master ]
paths: [ 'packages/phrases/**' ]
pull_request:
branches: [ master ]
paths: [ 'packages/phrases/**' ]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install packages
run: yarn
- name: Lint
working-directory: packages/phrases
run: yarn lint
- name: Build
working-directory: packages/phrases
run: yarn build

View file

@ -14,12 +14,14 @@
},
"dependencies": {
"@logto/essentials": "^1.1.0-rc.1",
"@logto/phrases": "^0.1.0",
"@logto/schemas": "^0.1.0",
"dayjs": "^1.10.5",
"decamelize": "^5.0.0",
"dotenv": "^10.0.0",
"formidable": "^1.2.2",
"got": "^11.8.2",
"i18next": "^20.3.5",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-logger": "^3.2.1",

View file

@ -1,7 +0,0 @@
export enum GuardErrorCode {
InvalidInput = 'guard.invalid_input',
}
export const guardErrorMessage: Record<GuardErrorCode, string> = {
[GuardErrorCode.InvalidInput]: 'The request input is invalid.',
};

View file

@ -1,7 +0,0 @@
export enum OidcErrorCode {
Aborted = 'oidc.aborted',
}
export const oidcErrorMessage: Record<OidcErrorCode, string> = {
[OidcErrorCode.Aborted]: 'The end-user aborted interaction.',
};

View file

@ -1,7 +0,0 @@
export enum RegisterErrorCode {
UsernameExists = 'register.username_exists',
}
export const registerErrorMessage: Record<RegisterErrorCode, string> = {
[RegisterErrorCode.UsernameExists]: 'The username already exists.',
};

View file

@ -1,11 +0,0 @@
export enum SignInErrorCode {
InvalidCredentials = 'sign_in.invalid_credentials',
InvalidSignInMethod = 'sign_in.invalid_sign_in_method',
InsufficientInfo = 'sign_in.insufficient_info',
}
export const signInErrorMessage: Record<SignInErrorCode, string> = {
[SignInErrorCode.InvalidCredentials]: 'Invalid credentials. Please check your input.',
[SignInErrorCode.InvalidSignInMethod]: 'Current sign-in method is not available.',
[SignInErrorCode.InsufficientInfo]: 'Insufficent sign-in info.',
};

View file

@ -1,7 +0,0 @@
export enum SwaggerErrorCode {
InvalidZodType = 'swagger.invalid_zod_type',
}
export const swaggerErrorMessage: Record<SwaggerErrorCode, string> = {
[SwaggerErrorCode.InvalidZodType]: 'Invalid Zod type, please check route guard config.',
};

View file

@ -1,19 +1,19 @@
import pick from 'lodash.pick';
import { requestErrorMessage } from './message';
import { RequestErrorBody, RequestErrorCode, RequestErrorMetadata } from './types';
import i18next from 'i18next';
import { LogtoErrorCode } from '@logto/phrases';
import { RequestErrorBody, RequestErrorMetadata } from './types';
export * from './types';
export * from './message';
export default class RequestError extends Error {
code: RequestErrorCode;
code: LogtoErrorCode;
status: number;
expose: boolean;
data: unknown;
constructor(input: RequestErrorMetadata | RequestErrorCode, data?: unknown) {
constructor(input: RequestErrorMetadata | LogtoErrorCode, data?: unknown) {
const { code, status = 400 } = typeof input === 'string' ? { code: input } : input;
const message = requestErrorMessage[code];
const message = i18next.t<string, LogtoErrorCode>(code);
super(message);

View file

@ -1,14 +0,0 @@
import { RequestErrorCode } from './types';
import { guardErrorMessage } from './collection/guard-errors';
import { oidcErrorMessage } from './collection/oidc-errors';
import { registerErrorMessage } from './collection/register-errors';
import { swaggerErrorMessage } from './collection/swagger-errors';
import { signInErrorMessage } from './collection/sign-in-errors';
export const requestErrorMessage: Record<RequestErrorCode, string> = {
...guardErrorMessage,
...oidcErrorMessage,
...registerErrorMessage,
...swaggerErrorMessage,
...signInErrorMessage,
};

View file

@ -1,20 +1,7 @@
import { GuardErrorCode } from './collection/guard-errors';
import { OidcErrorCode } from './collection/oidc-errors';
import { RegisterErrorCode } from './collection/register-errors';
import { SwaggerErrorCode } from './collection/swagger-errors';
import { SignInErrorCode } from './collection/sign-in-errors';
export { GuardErrorCode, OidcErrorCode, SwaggerErrorCode, RegisterErrorCode, SignInErrorCode };
export type RequestErrorCode =
| GuardErrorCode
| OidcErrorCode
| RegisterErrorCode
| SwaggerErrorCode
| SignInErrorCode;
import { LogtoErrorCode } from '@logto/phrases';
export type RequestErrorMetadata = {
code: RequestErrorCode;
code: LogtoErrorCode;
status?: number;
};

View file

@ -6,12 +6,14 @@ import dotenv from 'dotenv';
dotenv.config();
import Koa from 'koa';
import initApp from './init';
import initI18n from './init/i18n';
import initApp from './init/app';
const app = new Koa();
(async () => {
try {
await initI18n();
await initApp(app);
} catch (error: unknown) {
console.log('Error while initializing app', error);

View file

@ -0,0 +1,9 @@
import i18next from 'i18next';
import resources from '@logto/phrases';
export default async function initI18n() {
await i18next.init({
lng: 'en',
resources,
});
}

View file

@ -1,4 +1,4 @@
import RequestError, { GuardErrorCode } from '@/errors/RequestError';
import RequestError from '@/errors/RequestError';
import { has } from '@logto/essentials';
import { Middleware } from 'koa';
import koaBody from 'koa-body';
@ -69,7 +69,7 @@ export default function koaGuard<
params: params?.parse(ctx.params),
} as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do t His since it's too complicated for TS
} catch (error: unknown) {
throw new RequestError(GuardErrorCode.InvalidInput, error);
throw new RequestError('guard.invalid_input', error);
}
await next();

View file

@ -5,7 +5,7 @@ import { hasUser, hasUserWithId, insertUser } from '@/queries/user';
import { customAlphabet, nanoid } from 'nanoid';
import { PasswordEncryptionMethod } from '@logto/schemas';
import koaGuard from '@/middleware/koa-guard';
import RequestError, { RegisterErrorCode } from '@/errors/RequestError';
import RequestError from '@/errors/RequestError';
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const userId = customAlphabet(alphabet, 12);
@ -37,7 +37,7 @@ export default function registerRoutes() {
const { username, password } = ctx.guard.body;
if (await hasUser(username)) {
throw new RequestError(RegisterErrorCode.UsernameExists);
throw new RequestError('register.username_exists');
}
const id = await generateUserId();

View file

@ -6,7 +6,8 @@ import { findUserByUsername } from '@/queries/user';
import { Provider } from 'oidc-provider';
import { conditional } from '@logto/essentials';
import koaGuard from '@/middleware/koa-guard';
import RequestError, { OidcErrorCode, SignInErrorCode } from '@/errors/RequestError';
import RequestError from '@/errors/RequestError';
import { LogtoErrorCode } from '@logto/phrases';
export default function signInRoutes(provider: Provider) {
const router = new Router();
@ -22,7 +23,7 @@ export default function signInRoutes(provider: Provider) {
if (name === 'login') {
const { username, password } = ctx.guard.body;
assert(username && password, new RequestError(SignInErrorCode.InsufficientInfo));
assert(username && password, new RequestError('sign_in.insufficient_info'));
try {
const { id, passwordEncrypted, passwordEncryptionMethod, passwordEncryptionSalt } =
@ -30,12 +31,12 @@ export default function signInRoutes(provider: Provider) {
assert(
passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt,
new RequestError(SignInErrorCode.InvalidSignInMethod)
new RequestError('sign_in.invalid_sign_in_method')
);
assert(
encryptPassword(id, password, passwordEncryptionSalt, passwordEncryptionMethod) ===
passwordEncrypted,
new RequestError(SignInErrorCode.InvalidCredentials)
new RequestError('sign_in.invalid_credentials')
);
const redirectTo = await provider.interactionResult(
@ -49,7 +50,7 @@ export default function signInRoutes(provider: Provider) {
ctx.body = { redirectTo };
} catch (error: unknown) {
if (!(error instanceof RequestError)) {
throw new RequestError(SignInErrorCode.InvalidCredentials);
throw new RequestError('sign_in.invalid_credentials');
}
throw error;
@ -98,8 +99,9 @@ export default function signInRoutes(provider: Provider) {
router.post('/sign-in/abort', async (ctx) => {
await provider.interactionDetails(ctx.req, ctx.res);
const error: LogtoErrorCode = 'oidc.aborted';
const redirectTo = await provider.interactionResult(ctx.req, ctx.res, {
error: OidcErrorCode.Aborted,
error,
});
ctx.body = { redirectTo };
});

View file

@ -1,6 +1,6 @@
import { OpenAPIV3 } from 'openapi-types';
import { ZodArray, ZodBoolean, ZodNumber, ZodObject, ZodOptional, ZodString } from 'zod';
import RequestError, { SwaggerErrorCode } from '@/errors/RequestError';
import RequestError from '@/errors/RequestError';
import { conditional } from '@logto/essentials';
export const zodTypeToSwagger = (config: unknown): OpenAPIV3.SchemaObject => {
@ -46,5 +46,5 @@ export const zodTypeToSwagger = (config: unknown): OpenAPIV3.SchemaObject => {
};
}
throw new RequestError(SwaggerErrorCode.InvalidZodType, config);
throw new RequestError('swagger.invalid_zod_type', config);
};

View file

@ -0,0 +1,11 @@
# `@logto/phrases`
> TODO: description
## Usage
```
const phrases = require('@logto/phrases');
// TODO: DEMONSTRATE API
```

View file

@ -0,0 +1,43 @@
{
"name": "@logto/phrases",
"version": "0.1.0",
"description": "Logto shared phrases (l10n).",
"author": "Gao Sun <gaosun.dev@gmail.com>",
"homepage": "https://github.com/logto-io/logto#readme",
"license": "UNLICENSED",
"main": "lib/index.js",
"private": true,
"files": [
"lib"
],
"publishConfig": {
"registry": "https://registry.yarnpkg.com"
},
"repository": {
"type": "git",
"url": "git+https://github.com/logto-io/logto.git"
},
"scripts": {
"build": "rm -rf lib/ && tsc",
"lint": "eslint --format pretty \"src/**\"",
"prepack": "yarn build"
},
"bugs": {
"url": "https://github.com/logto-io/logto/issues"
},
"devDependencies": {
"@logto/eslint-config": "^0.1.0-rc.14",
"@logto/ts-config": "^0.1.0-rc.14",
"eslint": "^7.31.0",
"eslint-formatter-pretty": "^4.1.0",
"prettier": "^2.3.2",
"typescript": "^4.3.5"
},
"eslintConfig": {
"extends": "@logto"
},
"prettier": "@logto/eslint-config/.prettierrc",
"dependencies": {
"@logto/schemas": "^0.1.0"
}
}

View file

@ -0,0 +1,12 @@
import en from './locales/en';
import zhCN from './locales/zh-cn';
import { Normalize, Resource } from './types';
export type LogtoErrorCode = Normalize<typeof en.errors>;
const resource: Resource = {
en,
'zh-CN': zhCN,
};
export default resource;

View file

@ -0,0 +1,39 @@
const translation = {
sign_in: {
title: 'Sign In',
loading: 'Signing in...',
error: 'Username or password is invalid.',
username: 'Username',
password: 'Password',
},
register: {
create_account: 'Create an Account',
},
};
const errors = {
guard: {
invalid_input: 'The request input is invalid.',
},
oidc: {
aborted: 'The end-user aborted interaction.',
},
register: {
username_exists: 'The username already exists.',
},
sign_in: {
invalid_credentials: 'Invalid credentials. Please check your input.',
invalid_sign_in_method: 'Current sign-in method is not available.',
insufficient_info: 'Insufficent sign-in info.',
},
swagger: {
invalid_zod_type: 'Invalid Zod type, please check route guard config.',
},
};
const en = Object.freeze({
translation,
errors,
});
export default en;

View file

@ -0,0 +1,41 @@
import en from './en';
const translation = {
sign_in: {
title: '登录',
loading: '登录中...',
error: '用户名或密码错误。',
username: '用户名',
password: '密码',
},
register: {
create_account: '新用户注册',
},
};
const errors = {
guard: {
invalid_input: '请求内容有误。',
},
oidc: {
aborted: '用户终止了交互。',
},
register: {
username_exists: '用户名已存在。',
},
sign_in: {
invalid_credentials: '用户名或密码错误,请检查您的输入。',
invalid_sign_in_method: '当前登录方式不可用。',
insufficient_info: '登录信息缺失,请检查您的输入。',
},
swagger: {
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
},
};
const zhCN: typeof en = Object.freeze({
translation,
errors,
});
export default zhCN;

View file

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/consistent-indexed-object-style */
/* Copied from i18next/index.d.ts */
export interface Resource {
[language: string]: ResourceLanguage;
}
export interface ResourceLanguage {
[namespace: string]: ResourceKey;
}
export type ResourceKey =
| string
| {
[key: string]: any;
};
/* Copied from react-i18next/ts4.1/index.d.ts */
// Normalize single namespace
type AppendKeys<K1, K2> = `${K1 & string}.${K2 & string}`;
type AppendKeys2<K1, K2> = `${K1 & string}.${Exclude<K2, keyof any[]> & string}`;
type Normalize2<T, K = keyof T> = K extends keyof T
? T[K] extends Record<string, any>
? T[K] extends readonly any[]
? AppendKeys2<K, keyof T[K]> | AppendKeys2<K, Normalize2<T[K]>>
: AppendKeys<K, keyof T[K]> | AppendKeys<K, Normalize2<T[K]>>
: never
: never;
export type Normalize<T> = keyof T | Normalize2<T>;

View file

@ -0,0 +1,8 @@
{
"extends": "@logto/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"declaration": true
},
"include": ["src"]
}

View file

@ -15,6 +15,7 @@
"test": "razzle test --env=jsdom"
},
"dependencies": {
"@logto/phrases": "^0.1.0",
"classnames": "^2.3.1",
"i18next": "^20.3.3",
"i18next-browser-languagedetector": "^6.1.2",

View file

@ -1,10 +1,10 @@
import React, { ReactChildren } from 'react';
import React, { ReactChild } from 'react';
import classNames from 'classnames';
import styles from './index.module.scss';
export type Props = {
className?: string;
children: ReactChildren;
children: ReactChild;
href: string;
};

View file

@ -2,7 +2,7 @@
// eslint-disable-next-line import/no-unassigned-import
import 'react-i18next';
import en from '@/locales/en.json';
import en from '@logto/phrases/lib/locales/en.js';
declare module 'react-i18next' {
interface CustomTypeOptions {

View file

@ -1,18 +1,14 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import en from '@/locales/en.json';
import zhCN from '@/locales/zh-CN.json';
import resources from '@logto/phrases';
const initI18n = () => {
void i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
en,
'zh-CN': zhCN,
},
resources,
fallbackLng: 'en',
interpolation: {
escapeValue: false,

View file

@ -1,10 +0,0 @@
{
"translation": {
"sign_in": "Sign In",
"sign_in.loading": "Signing in...",
"sign_in.error": "Username or password is invalid.",
"sign_in.username": "Username",
"sign_in.password": "Password",
"register.create_account": "Create an Account"
}
}

View file

@ -1,10 +0,0 @@
{
"translation": {
"sign_in": "登录",
"sign_in.loading": "登录中...",
"sign_in.error": "用户名或密码错误。",
"sign_in.username": "用户名",
"sign_in.password": "密码",
"register.create_account": "新用户注册"
}
}

View file

@ -33,7 +33,7 @@ const Home = () => {
<Input
autoComplete="username"
isDisabled={isLoading}
placeholder={t('sign_in.username')}
placeholder={t('sign_in.password')}
value={username}
onChange={setUsername}
/>
@ -50,7 +50,7 @@ const Home = () => {
)}
<Button
isDisabled={isLoading}
value={isLoading ? t('sign_in.loading') : t('sign_in')}
value={isLoading ? t('sign_in.loading') : t('sign_in.title')}
onClick={signIn}
/>
<TextLink className={styles.createAccount} href="/register">

View file

@ -6294,7 +6294,7 @@ eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint@^7.30.0:
eslint@^7.30.0, eslint@^7.31.0:
version "7.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==
@ -7748,7 +7748,7 @@ i18next-browser-languagedetector@^6.1.2:
dependencies:
"@babel/runtime" "^7.14.6"
i18next@^20.3.3:
i18next@^20.3.3, i18next@^20.3.5:
version "20.3.5"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.5.tgz#14308b79a3f1cafb24fdcd8e182d3673baf1e979"
integrity sha512-//MGeU6n4TencJmCgG+TCrpdgAD/NDEU/KfKQekYbJX6QV7sD/NjWQdVdBi+bkT0snegnSoB7QhjSeatrk3a0w==