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:
parent
3231be014a
commit
f0f5ac111c
3 changed files with 139 additions and 108 deletions
|
@ -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`, `
|
||||||
|
);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
104
packages/core/src/queries/oidc-adapter.ts
Normal file
104
packages/core/src/queries/oidc-adapter.ts
Normal 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}
|
||||||
|
`);
|
||||||
|
};
|
Loading…
Reference in a new issue