0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor: re-org OIDC adapter

This commit is contained in:
Gao Sun 2021-07-04 21:52:20 +08:00
parent 3231be014a
commit f0f5ac111c
No known key found for this signature in database
GPG key ID: 0F0EFA2E36639F31
3 changed files with 139 additions and 108 deletions

View file

@ -1,10 +1,13 @@
import { IdentifierSqlTokenType, sql, ValueExpressionType } from 'slonik'; import { IdentifierSqlTokenType, sql } from 'slonik';
type Table = { table: string; fields: Record<string, string> }; type Table = { table: string; fields: Record<string, string> };
type FieldIdentifiers<Key extends string | number | symbol> = { type FieldIdentifiers<Key extends string | number | symbol> = {
[key in Key]: IdentifierSqlTokenType; [key in Key]: IdentifierSqlTokenType;
}; };
const convertToPrimitive = <T>(value: T) =>
value !== null && typeof value === 'object' ? JSON.stringify(value) : value;
export const convertToIdentifiers = <T extends Table>( export const convertToIdentifiers = <T extends Table>(
{ table, fields }: T, { table, fields }: T,
withPrefix = false withPrefix = false
@ -20,14 +23,23 @@ export const convertToIdentifiers = <T extends Table>(
), ),
}); });
export const insertInto = <T extends string>( export const insertInto = <Type, Key extends keyof Type = keyof Type>(
table: IdentifierSqlTokenType, table: IdentifierSqlTokenType,
fields: FieldIdentifiers<T>, fields: FieldIdentifiers<Key>,
fieldKeys: readonly T[], fieldKeys: readonly Key[],
value: { [key in T]?: ValueExpressionType } value: { [key in Key]?: Type[key] }
) => sql` ) => sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)}) insert into ${table} (${sql.join(
fieldKeys.map((key) => fields[key]),
sql`, `
)})
values (${sql.join( values (${sql.join(
fieldKeys.map((key) => value[key] ?? null), fieldKeys.map((key) => convertToPrimitive(value[key] ?? null)),
sql`, ` sql`, `
)})`; )})`;
export const setExcluded = (...fields: IdentifierSqlTokenType[]) =>
sql.join(
fields.map((field) => sql`${field}=excluded.${field}`),
sql`, `
);

View file

@ -1,106 +1,21 @@
import dayjs from 'dayjs';
import { AdapterFactory } from 'oidc-provider'; import { AdapterFactory } from 'oidc-provider';
import { IdentifierSqlTokenType, sql, ValueExpressionType } from 'slonik';
import { conditional } from '@logto/essentials';
import { import {
OidcModelInstances, consumeInstanceById,
OidcModelInstanceDBEntry, destoryInstanceById,
OidcModelInstancePayload, findPayloadById,
} from '@logto/schemas'; findPayloadByPayloadField,
import pool from '@/database/pool'; revokeInstanceByGrantId,
import { convertToIdentifiers } from '@/database/utils'; upsertInstance,
} from '@/queries/oidc-adapter';
export default function postgresAdapter(modelName: string) { export default function postgresAdapter(modelName: string): ReturnType<AdapterFactory> {
const { table, fields } = convertToIdentifiers(OidcModelInstances); return {
upsert: async (id, payload, expiresIn) => upsertInstance(modelName, id, payload, expiresIn),
type WithConsumed<T> = T & { consumed?: boolean }; find: async (id) => findPayloadById(modelName, id),
const withConsumed = <T>(data: T, consumedAt?: number): WithConsumed<T> => ({ findByUserCode: async (userCode) => findPayloadByPayloadField(modelName, 'userCode', userCode),
...data, findByUid: async (uid) => findPayloadByPayloadField(modelName, 'uid', uid),
...(consumedAt ? { consumed: true } : undefined), consume: async (id) => consumeInstanceById(modelName, id),
}); destroy: async (id) => destoryInstanceById(modelName, id),
type QueryResult = Pick<OidcModelInstanceDBEntry, 'payload' | 'consumedAt'>; revokeByGrantId: async (grantId) => revokeInstanceByGrantId(modelName, grantId),
const convertResult = (result: QueryResult | null) =>
conditional(result && withConsumed(result.payload, result.consumedAt));
const setExcluded = (...fields: IdentifierSqlTokenType[]) =>
sql.join(
fields.map((field) => sql`${field}=excluded.${field}`),
sql`, `
);
const findByField = async <T extends ValueExpressionType>(
field: IdentifierSqlTokenType,
value: T
) => {
const result = await pool.maybeOne<QueryResult>(sql`
select ${fields.payload}, ${fields.consumedAt}
from ${table}
where ${fields.modelName}=${modelName}
and ${field}=${value}
`);
return convertResult(result);
}; };
const findByPayloadField = async <
T extends ValueExpressionType,
Field extends keyof OidcModelInstancePayload
>(
field: Field,
value: T
) => {
const result = await pool.maybeOne<QueryResult>(sql`
select ${fields.payload}, ${fields.consumedAt}
from ${table}
where ${fields.modelName}=${modelName}
and ${fields.payload}->>${field}=${value}
`);
return convertResult(result);
};
const adapter: ReturnType<AdapterFactory> = {
upsert: async (id, payload, expiresIn) => {
await pool.query(sql`
insert into ${table} (${sql.join(
[fields.modelName, fields.id, fields.payload, fields.expiresAt],
sql`, `
)})
values (
${modelName},
${id},
${JSON.stringify(payload)},
${dayjs().add(expiresIn, 'second').unix()}
)
on conflict (${fields.modelName}, ${fields.id}) do update
set ${setExcluded(fields.payload, fields.expiresAt)}
`);
},
find: async (id) => findByField(fields.id, id),
findByUserCode: async (userCode) => findByPayloadField('userCode', userCode),
findByUid: async (uid) => findByPayloadField('uid', uid),
consume: async (id) => {
await pool.query(sql`
update ${table}
set ${fields.consumedAt}=${dayjs().unix()}
where ${fields.modelName}=${modelName}
and ${fields.id}=${id}
`);
},
destroy: async (id) => {
await pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.id}=${id}
`);
},
revokeByGrantId: async (grantId) => {
await pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.payload}->>'grantId'=${grantId}
`);
},
};
return adapter;
} }

View file

@ -0,0 +1,104 @@
import pool from '@/database/pool';
import { convertToIdentifiers, insertInto, setExcluded } from '@/database/utils';
import { conditional } from '@logto/essentials';
import {
OidcModelInstanceDBEntry,
OidcModelInstancePayload,
OidcModelInstances,
} from '@logto/schemas';
import dayjs from 'dayjs';
import { sql, ValueExpressionType } from 'slonik';
export type WithConsumed<T> = T & { consumed?: boolean };
export type QueryResult = Pick<OidcModelInstanceDBEntry, 'payload' | 'consumedAt'>;
const { table, fields } = convertToIdentifiers(OidcModelInstances);
const withConsumed = <T>(data: T, consumedAt?: number): WithConsumed<T> => ({
...data,
...(consumedAt ? { consumed: true } : undefined),
});
const convertResult = (result: QueryResult | null) =>
conditional(result && withConsumed(result.payload, result.consumedAt));
export const upsertInstance = async (
modelName: string,
id: string,
payload: OidcModelInstancePayload,
expiresIn: number
) => {
await pool.query(
sql`
${insertInto<OidcModelInstanceDBEntry>(
table,
fields,
['modelName', 'id', 'payload', 'expiresAt'],
{
modelName,
id,
payload,
expiresAt: dayjs().add(expiresIn, 'second').unix(),
}
)}
on conflict (${fields.modelName}, ${fields.id}) do update
set ${setExcluded(fields.payload, fields.expiresAt)}
`
);
};
const findByModel = (modelName: string) => sql`
select ${fields.payload}, ${fields.consumedAt}
from ${table}
where ${fields.modelName}=${modelName}
`;
export const findPayloadById = async (modelName: string, id: string) => {
const result = await pool.maybeOne<QueryResult>(sql`
${findByModel(modelName)}
and ${fields.id}=${id}
`);
return convertResult(result);
};
export const findPayloadByPayloadField = async <
T extends ValueExpressionType,
Field extends keyof OidcModelInstancePayload
>(
modelName: string,
field: Field,
value: T
) => {
const result = await pool.maybeOne<QueryResult>(sql`
${findByModel(modelName)}
and ${fields.payload}->>${field}=${value}
`);
return convertResult(result);
};
export const consumeInstanceById = async (modelName: string, id: string) => {
await pool.query(sql`
update ${table}
set ${fields.consumedAt}=${dayjs().unix()}
where ${fields.modelName}=${modelName}
and ${fields.id}=${id}
`);
};
export const destoryInstanceById = async (modelName: string, id: string) => {
await pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.id}=${id}
`);
};
export const revokeInstanceByGrantId = async (modelName: string, grantId: string) => {
await pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.payload}->>'grantId'=${grantId}
`);
};