0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

Merge pull request #2681 from logto-io/gao-refactor-log-types-2

refactor: log types 2
This commit is contained in:
Gao Sun 2022-12-21 11:01:52 +08:00 committed by GitHub
commit caa75422ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 88 additions and 44 deletions

View file

@ -8,13 +8,13 @@ import { logEventTitle } from '@/consts/logs';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
type: string; eventKey: string;
isSuccess: boolean; isSuccess: boolean;
to?: string; to?: string;
}; };
const EventName = ({ type, isSuccess, to }: Props) => { const EventName = ({ eventKey, isSuccess, to }: Props) => {
const title = logEventTitle[type] ?? type; const title = logEventTitle[eventKey] ?? eventKey;
return ( return (
<div className={styles.eventName}> <div className={styles.eventName}>

View file

@ -115,7 +115,7 @@ const AuditLogTable = ({ userId }: Props) => {
)} )}
{isLoading && <TableLoading columns={tableColumnCount} />} {isLoading && <TableLoading columns={tableColumnCount} />}
{logs?.length === 0 && <TableEmpty columns={tableColumnCount} />} {logs?.length === 0 && <TableEmpty columns={tableColumnCount} />}
{logs?.map(({ type, payload, createdAt, id }) => ( {logs?.map(({ key, payload, createdAt, id }) => (
<tr <tr
key={id} key={id}
className={tableStyles.clickable} className={tableStyles.clickable}
@ -124,7 +124,7 @@ const AuditLogTable = ({ userId }: Props) => {
}} }}
> >
<td> <td>
<EventName type={type} isSuccess={payload.result === LogResult.Success} /> <EventName eventKey={key} isSuccess={payload.result === LogResult.Success} />
</td> </td>
{showUserColumn && ( {showUserColumn && (
<td>{payload.userId ? <UserName userId={payload.userId} /> : '-'}</td> <td>{payload.userId ? <UserName userId={payload.userId} /> : '-'}</td>

View file

@ -1,6 +1,9 @@
import type { LogKey } from '@logto/schemas';
type LogEventTitle = Record<string, string>; type LogEventTitle = Record<string, string>;
export const logEventTitle: LogEventTitle = Object.freeze({ /** @deprecated Don't use or update. */
const logEventTitleLegacy: LogEventTitle = Object.freeze({
RegisterUsernamePassword: 'Register with username and password', RegisterUsernamePassword: 'Register with username and password',
RegisterEmailSendPasscode: 'Register with email (send passcode)', RegisterEmailSendPasscode: 'Register with email (send passcode)',
RegisterEmail: 'Register with email', RegisterEmail: 'Register with email',
@ -19,3 +22,12 @@ export const logEventTitle: LogEventTitle = Object.freeze({
RefreshTokenExchangeToken: 'Exchange token by refresh token', RefreshTokenExchangeToken: 'Exchange token by refresh token',
RevokeToken: 'Revoke token', RevokeToken: 'Revoke token',
}); });
export const logEventTitle: Record<string, string | undefined> & Partial<Record<LogKey, string>> =
Object.freeze({
...logEventTitleLegacy,
'ExchangeTokenBy.AuthorizationCode': 'Exchange token by auth code',
'ExchangeTokenBy.RefreshToken': 'Exchange token by refresh token',
'Interaction.Create': 'Interaction started',
'Interaction.End': 'Interaction ended',
});

View file

@ -57,11 +57,11 @@ const AuditLogDetails = () => {
<Card className={styles.header}> <Card className={styles.header}>
<EventIcon isSuccess={data.payload.result === 'Success'} /> <EventIcon isSuccess={data.payload.result === 'Success'} />
<div className={styles.content}> <div className={styles.content}>
<div className={styles.eventName}>{logEventTitle[data.type]}</div> <div className={styles.eventName}>{logEventTitle[data.key]}</div>
<div className={styles.basicInfo}> <div className={styles.basicInfo}>
<div className={styles.infoItem}> <div className={styles.infoItem}>
<div className={styles.label}>{t('log_details.event_type')}</div> <div className={styles.label}>{t('log_details.event_key')}</div>
<div>{data.type}</div> <div>{data.key}</div>
</div> </div>
<div className={styles.infoItem}> <div className={styles.infoItem}>
<div className={styles.label}>{t('log_details.application')}</div> <div className={styles.label}>{t('log_details.application')}</div>

View file

@ -58,7 +58,7 @@ describe('koaLog middleware', () => {
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type, key: type,
payload: { payload: {
...mockPayload, ...mockPayload,
...additionalMockPayload, ...additionalMockPayload,
@ -105,7 +105,7 @@ describe('koaLog middleware', () => {
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type, key: type,
payload: { payload: {
...mockPayload, ...mockPayload,
result: LogResult.Error, result: LogResult.Error,
@ -139,7 +139,7 @@ describe('koaLog middleware', () => {
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type, key: type,
payload: { payload: {
...mockPayload, ...mockPayload,
result: LogResult.Error, result: LogResult.Error,

View file

@ -71,7 +71,7 @@ const initLogger = (basePayload?: Readonly<BaseLogPayload>) => {
await insertLog({ await insertLog({
id: nanoid(), id: nanoid(),
type: logger.type, key: logger.type,
payload: { payload: {
...logger.basePayload, ...logger.basePayload,
...logger.payload, ...logger.payload,

View file

@ -54,7 +54,7 @@ describe('koaAuditLog middleware', () => {
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type: logKey, key: logKey,
payload: { payload: {
...mockPayload, ...mockPayload,
...additionalMockPayload, ...additionalMockPayload,
@ -93,12 +93,12 @@ describe('koaAuditLog middleware', () => {
expect(insertLog).toHaveBeenCalledWith({ expect(insertLog).toHaveBeenCalledWith({
id: nanoIdMock, id: nanoIdMock,
type: logKey, key: logKey,
payload: basePayload, payload: basePayload,
}); });
expect(insertLog).toHaveBeenCalledWith({ expect(insertLog).toHaveBeenCalledWith({
id: nanoIdMock, id: nanoIdMock,
type: logKey, key: logKey,
payload: { payload: {
...basePayload, ...basePayload,
...additionalMockPayload, ...additionalMockPayload,
@ -139,7 +139,7 @@ describe('koaAuditLog middleware', () => {
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type: logKey, key: logKey,
payload: { payload: {
...mockPayload, ...mockPayload,
key: logKey, key: logKey,
@ -176,7 +176,7 @@ describe('koaAuditLog middleware', () => {
expect(insertLog).toHaveBeenCalledTimes(2); expect(insertLog).toHaveBeenCalledTimes(2);
expect(insertLog).toBeCalledWith({ expect(insertLog).toBeCalledWith({
id: nanoIdMock, id: nanoIdMock,
type: logKey, key: logKey,
payload: { payload: {
...mockPayload, ...mockPayload,
key: logKey, key: logKey,

View file

@ -139,7 +139,7 @@ export default function koaAuditLog<
entries.map(async ({ payload }) => { entries.map(async ({ payload }) => {
return insertLog({ return insertLog({
id: nanoid(), id: nanoid(),
type: payload.key, key: payload.key,
payload: { ip, userAgent, ...payload }, payload: { ip, userAgent, ...payload },
}); });
}) })

View file

@ -12,15 +12,15 @@ const { table, fields } = convertToIdentifiers(Logs);
export const insertLog = buildInsertInto<CreateLog>(Logs); export const insertLog = buildInsertInto<CreateLog>(Logs);
export type LogCondition = { export type LogCondition = {
logType?: string; logKey?: string;
applicationId?: string; applicationId?: string;
userId?: string; userId?: string;
}; };
const buildLogConditionSql = (logCondition: LogCondition) => const buildLogConditionSql = (logCondition: LogCondition) =>
conditionalSql(logCondition, ({ logType, applicationId, userId }) => { conditionalSql(logCondition, ({ logKey, applicationId, userId }) => {
const subConditions = [ const subConditions = [
conditionalSql(logType, (logType) => sql`${fields.type}=${logType}`), conditionalSql(logKey, (logKey) => sql`${fields.key}=${logKey}`),
conditionalSql(userId, (userId) => sql`${fields.payload}->>'userId'=${userId}`), conditionalSql(userId, (userId) => sql`${fields.payload}->>'userId'=${userId}`),
conditionalSql( conditionalSql(
applicationId, applicationId,
@ -59,7 +59,7 @@ export const getDailyActiveUserCountsByTimeInterval = async (
from ${table} from ${table}
where ${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000) where ${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000)
and ${fields.createdAt} <= to_timestamp(${endTimeInclusive}::double precision / 1000) and ${fields.createdAt} <= to_timestamp(${endTimeInclusive}::double precision / 1000)
and ${fields.type} like ${`${token.Flow.ExchangeTokenBy}.%`} and ${fields.key} like ${`${token.Flow.ExchangeTokenBy}.%`}
and ${fields.payload}->>'result' = 'Success' and ${fields.payload}->>'result' = 'Success'
group by date(${fields.createdAt}) group by date(${fields.createdAt})
`); `);
@ -73,6 +73,6 @@ export const countActiveUsersByTimeInterval = async (
from ${table} from ${table}
where ${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000) where ${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000)
and ${fields.createdAt} <= to_timestamp(${endTimeInclusive}::double precision / 1000) and ${fields.createdAt} <= to_timestamp(${endTimeInclusive}::double precision / 1000)
and ${fields.type} like ${`${token.Flow.ExchangeTokenBy}.%`} and ${fields.key} like ${`${token.Flow.ExchangeTokenBy}.%`}
and ${fields.payload}->>'result' = 'Success' and ${fields.payload}->>'result' = 'Success'
`); `);

View file

@ -4,7 +4,7 @@ import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta; const { jest } = import.meta;
const mockBody = { type: 'a', payload: {}, createdAt: 123 }; const mockBody = { key: 'a', payload: {}, createdAt: 123 };
const mockLog = { id: '1', ...mockBody }; const mockLog = { id: '1', ...mockBody };
const mockLogs = [mockLog, { id: '2', ...mockBody }]; const mockLogs = [mockLog, { id: '2', ...mockBody }];
@ -28,15 +28,15 @@ describe('logRoutes', () => {
it('should call countLogs and findLogs with correct parameters', async () => { it('should call countLogs and findLogs with correct parameters', async () => {
const userId = 'userIdValue'; const userId = 'userIdValue';
const applicationId = 'foo'; const applicationId = 'foo';
const logType = 'SignInUsernamePassword'; const logKey = 'SignInUsernamePassword';
const page = 1; const page = 1;
const pageSize = 5; const pageSize = 5;
await logRequest.get( await logRequest.get(
`/logs?userId=${userId}&applicationId=${applicationId}&logType=${logType}&page=${page}&page_size=${pageSize}` `/logs?userId=${userId}&applicationId=${applicationId}&logKey=${logKey}&page=${page}&page_size=${pageSize}`
); );
expect(countLogs).toHaveBeenCalledWith({ userId, applicationId, logType }); expect(countLogs).toHaveBeenCalledWith({ userId, applicationId, logKey });
expect(findLogs).toHaveBeenCalledWith(5, 0, { userId, applicationId, logType }); expect(findLogs).toHaveBeenCalledWith(5, 0, { userId, applicationId, logKey });
}); });
it('should return correct response', async () => { it('should return correct response', async () => {

View file

@ -15,19 +15,19 @@ export default function logRoutes<T extends AuthedRouter>(router: T) {
query: object({ query: object({
userId: string().optional(), userId: string().optional(),
applicationId: string().optional(), applicationId: string().optional(),
logType: string().optional(), logKey: string().optional(),
}), }),
}), }),
async (ctx, next) => { async (ctx, next) => {
const { limit, offset } = ctx.pagination; const { limit, offset } = ctx.pagination;
const { const {
query: { userId, applicationId, logType }, query: { userId, applicationId, logKey },
} = ctx.guard; } = ctx.guard;
// TODO: @Gao refactor like user search // TODO: @Gao refactor like user search
const [{ count }, logs] = await Promise.all([ const [{ count }, logs] = await Promise.all([
countLogs({ logType, applicationId, userId }), countLogs({ logKey, applicationId, userId }),
findLogs(limit, offset, { logType, userId, applicationId }), findLogs(limit, offset, { logKey, userId, applicationId }),
]); ]);
// Return totalCount to pagination middleware // Return totalCount to pagination middleware

View file

@ -20,7 +20,7 @@ describe('admin console logs (legacy)', () => {
const logs = await getLogs(); const logs = await getLogs();
const registerLog = logs.filter( const registerLog = logs.filter(
({ type, payload }) => type === 'RegisterUsernamePassword' && payload.username === username ({ key, payload }) => key === 'RegisterUsernamePassword' && payload.username === username
); );
expect(registerLog.length).toBeGreaterThan(0); expect(registerLog.length).toBeGreaterThan(0);

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: 'Zurück zu {{name}}', back_to_user: 'Zurück zu {{name}}',
success: 'Erfolgreich', success: 'Erfolgreich',
failed: 'Fehlgeschlagen', failed: 'Fehlgeschlagen',
event_type: 'Event Typ', event_key: 'Event Key', // UNTRANSLATED
application: 'Anwendung', application: 'Anwendung',
ip_address: 'IP Adresse', ip_address: 'IP Adresse',
user: 'Benutzer', user: 'Benutzer',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: 'Back to {{name}}', back_to_user: 'Back to {{name}}',
success: 'Success', success: 'Success',
failed: 'Failed', failed: 'Failed',
event_type: 'Event type', event_key: 'Event Key',
application: 'Application', application: 'Application',
ip_address: 'IP address', ip_address: 'IP address',
user: 'User', user: 'User',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: 'Retour à {{name}}', back_to_user: 'Retour à {{name}}',
success: 'Succès', success: 'Succès',
failed: 'Échoué', failed: 'Échoué',
event_type: "Type d'événement", event_key: 'Event Key', // UNTRANSLATED
application: 'Application', application: 'Application',
ip_address: 'Addresse IP', ip_address: 'Addresse IP',
user: 'Utilisateur', user: 'Utilisateur',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: '{{name}}으로 돌아가기', back_to_user: '{{name}}으로 돌아가기',
success: '성공', success: '성공',
failed: '실패', failed: '실패',
event_type: '활동 종류', event_key: 'Event Key', // UNTRANSLATED
application: '어플리케이션', application: '어플리케이션',
ip_address: 'IP 주소', ip_address: 'IP 주소',
user: '사용자', user: '사용자',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: 'Voltar para {{name}}', back_to_user: 'Voltar para {{name}}',
success: 'Sucesso', success: 'Sucesso',
failed: 'Falhou', failed: 'Falhou',
event_type: 'Tipo de evento', event_key: 'Event Key', // UNTRANSLATED
application: 'Aplicativo', application: 'Aplicativo',
ip_address: 'Endereço de IP', ip_address: 'Endereço de IP',
user: 'Usuário', user: 'Usuário',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: 'De volta a {{name}}', back_to_user: 'De volta a {{name}}',
success: 'Sucesso', success: 'Sucesso',
failed: 'Falha', failed: 'Falha',
event_type: 'Tipo de evento', event_key: 'Event Key', // UNTRANSLATED
application: 'Aplicação', application: 'Aplicação',
ip_address: 'Endereço IP', ip_address: 'Endereço IP',
user: 'Utilizador', user: 'Utilizador',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: '{{name}}e geri dön', back_to_user: '{{name}}e geri dön',
success: 'Başarılı', success: 'Başarılı',
failed: 'Başarısız', failed: 'Başarısız',
event_type: 'Etkinlik tipi', event_key: 'Event Key', // UNTRANSLATED
application: 'Uygulama', application: 'Uygulama',
ip_address: 'IP adresi', ip_address: 'IP adresi',
user: 'Kullanıcı', user: 'Kullanıcı',

View file

@ -3,7 +3,7 @@ const log_details = {
back_to_user: '返回 {{name}}', back_to_user: '返回 {{name}}',
success: '成功', success: '成功',
failed: '失败', failed: '失败',
event_type: '事件类型', event_key: 'Event Key', // UNTRANSLATED
application: '应用', application: '应用',
ip_address: 'IP 地址', ip_address: 'IP 地址',
user: '用户', user: '用户',

View file

@ -0,0 +1,32 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
-- Update metadata
alter table logs rename column type to key;
alter table logs alter column key type varchar(128);
alter index logs__type rename to logs__key;
-- Update token exchange keys
update logs set "key" = 'ExchangeTokenBy.AuthorizationCode' where "key" = 'CodeExchangeToken';
update logs set "key" = 'ExchangeTokenBy.RefreshToken' where "key" = 'RefreshTokenExchangeToken';
`);
},
down: async (pool) => {
await pool.query(sql`
-- Update token exchange keys
update logs set "key" = 'CodeExchangeToken' where "key" = 'ExchangeTokenBy.AuthorizationCode';
update logs set "key" = 'RefreshTokenExchangeToken' where "key" = 'ExchangeTokenBy.RefreshToken';
-- Update metadata
alter table logs alter column key type varchar(64);
alter table logs rename column key to type;
alter index logs__key rename to logs__type;
`);
},
};
export default alteration;

View file

@ -1,13 +1,13 @@
create table logs create table logs
( (
id varchar(21) not null, id varchar(21) not null,
type varchar(64) not null, key varchar(128) not null,
payload jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb, payload jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb,
created_at timestamptz not null default (now()), created_at timestamptz not null default (now()),
primary key (id) primary key (id)
); );
create index logs__type on logs (type); create index logs__key on logs (key);
create index logs__created_at on logs (created_at); create index logs__created_at on logs (created_at);
create index logs__user_id on logs ((payload->>'user_id') nulls last); create index logs__user_id on logs ((payload->>'user_id') nulls last);
create index logs__application_id on logs ((payload->>'application_id') nulls last); create index logs__application_id on logs ((payload->>'application_id') nulls last);