0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor: remove magic links

This commit is contained in:
Gao Sun 2024-01-29 20:21:45 +08:00
parent 0a20b5a6f8
commit 6d0f95739c
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
9 changed files with 54 additions and 122 deletions

View file

@ -1,11 +1,5 @@
import { type GeneratedSchema } from '@logto/schemas';
import {
type SchemaLike,
conditionalSql,
convertToIdentifiers,
type Table,
type FieldIdentifiers,
} from '@logto/shared';
import { type SchemaLike, conditionalSql, convertToIdentifiers, type Table } from '@logto/shared';
import { type SqlSqlToken, sql } from 'slonik';
/**
@ -62,48 +56,3 @@ export const expandFields = <Keys extends string>(schema: Table<Keys>, tablePref
const { fields } = convertToIdentifiers(schema, tablePrefix);
return sql.join(Object.values(fields), sql`, `);
};
/**
* Given a set of identifiers, build a SQL that converts them into a JSON object by mapping
* the keys to the values.
*
* @example
* ```ts
* buildJsonObjectSql({
* id: sql.identifier(['id']),
* firstName: sql.identifier(['first_name']),
* lastName: sql.identifier(['last_name']),
* createdAt: sql.identifier(['created_at']),
* );
* ```
*
* will generate
*
* ```sql
* json_build_object(
* 'id', "id",
* 'firstName', "first_name",
* 'lastName', "last_name",
* 'createdAt', trunc(extract(epoch from "created_at") * 1000)
* )
* ```
*
* @remarks The values will be converted to epoch milliseconds if the key ends with `At` since
* slonik has a default parser that converts timestamps to epoch milliseconds, but it does not
* work for JSON objects.
*/
export const buildJsonObjectSql = <Identifiers extends FieldIdentifiers<string>>(
identifiers: Identifiers
) => sql`
json_build_object(
${sql.join(
Object.entries(identifiers).map(
([key, value]) =>
sql`${sql.literalValue(key)}, ${
key.endsWith('At') ? sql`trunc(extract(epoch from ${value}) * 1000)` : value
}`
),
sql`, `
)}
)
`;

View file

@ -5,12 +5,9 @@ import {
type OrganizationInvitationEntity,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { appendPath, removeUndefinedKeys } from '@silverhand/essentials';
import { removeUndefinedKeys } from '@silverhand/essentials';
import { EnvSet } from '#src/env-set/index.js';
import { getTenantEndpoint } from '#src/env-set/utils.js';
import RequestError from '#src/errors/RequestError/index.js';
import MagicLinkQueries from '#src/queries/magic-link.js';
import OrganizationQueries from '#src/queries/organization/index.js';
import { createUserQueries } from '#src/queries/user.js';
import type Queries from '#src/tenants/Queries.js';
@ -62,18 +59,11 @@ export class OrganizationInvitationLibrary {
return this.queries.pool.transaction(async (connection) => {
const organizationQueries = new OrganizationQueries(connection);
const magicLinkQueries = new MagicLinkQueries(connection);
const magicLink = await magicLinkQueries.insert({
id: generateStandardId(),
token: generateStandardId(32),
});
const invitation = await organizationQueries.invitations.insert({
id: generateStandardId(),
inviterId,
invitee,
organizationId,
magicLinkId: magicLink.id,
status: OrganizationInvitationStatus.Pending,
expiresAt,
});
@ -85,7 +75,7 @@ export class OrganizationInvitationLibrary {
}
if (!skipEmail) {
await this.sendEmail(invitee, magicLink.token);
await this.sendEmail(invitee);
}
// Additional query to get the full invitation data
@ -195,14 +185,13 @@ export class OrganizationInvitationLibrary {
});
}
protected async sendEmail(to: string, token: string) {
protected async sendEmail(to: string) {
const emailConnector = await this.connector.getMessageConnector(ConnectorType.Email);
return emailConnector.sendMessage({
to,
type: TemplateType.OrganizationInvitation,
payload: {
link: appendPath(getTenantEndpoint(this.tenantId, EnvSet.values), invitationLinkPath, token)
.href,
link: 'TODO',
},
});
}

View file

@ -1,19 +0,0 @@
import {
type CreateMagicLink,
type MagicLink,
type MagicLinkKeys,
MagicLinks,
} from '@logto/schemas';
import { type CommonQueryMethods } from 'slonik';
import SchemaQueries from '#src/utils/SchemaQueries.js';
export default class MagicLinkQueries extends SchemaQueries<
MagicLinkKeys,
CreateMagicLink,
MagicLink
> {
constructor(pool: CommonQueryMethods) {
super(pool, MagicLinks);
}
}

View file

@ -15,14 +15,13 @@ import {
type CreateOrganizationInvitation,
type OrganizationInvitation,
type OrganizationInvitationEntity,
MagicLinks,
OrganizationInvitationRoleRelations,
OrganizationInvitationStatus,
} from '@logto/schemas';
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
import { sql, type CommonQueryMethods } from 'slonik';
import { type SearchOptions, buildSearchSql, buildJsonObjectSql } from '#src/database/utils.js';
import { type SearchOptions, buildSearchSql } from '#src/database/utils.js';
import { TwoRelationsQueries } from '#src/utils/RelationQueries.js';
import SchemaQueries from '#src/utils/SchemaQueries.js';
@ -116,7 +115,6 @@ class OrganizationInvitationsQueries extends SchemaQueries<
search?: SearchOptions<OrganizationInvitationKeys>
) {
const { table, fields } = convertToIdentifiers(OrganizationInvitations, true);
const magicLinks = convertToIdentifiers(MagicLinks, true);
const roleRelations = convertToIdentifiers(OrganizationInvitationRoleRelations, true);
const roles = convertToIdentifiers(OrganizationRoles, true);
@ -143,20 +141,17 @@ class OrganizationInvitationsQueries extends SchemaQueries<
) order by ${roles.fields.name}
) filter (where ${roles.fields.id} is not null),
'[]'
) as "organizationRoles", -- left join could produce nulls
${buildJsonObjectSql(magicLinks.fields)} as "magicLink"
) as "organizationRoles" -- left join could produce nulls
from ${table}
left join ${roleRelations.table}
on ${roleRelations.fields.organizationInvitationId} = ${fields.id}
left join ${roles.table}
on ${roles.fields.id} = ${roleRelations.fields.organizationRoleId}
left join ${magicLinks.table}
on ${magicLinks.fields.id} = ${fields.magicLinkId}
${conditionalSql(invitationId, (id) => {
return sql`where ${fields.id} = ${id}`;
})}
${buildSearchSql(OrganizationInvitations, search)}
group by ${fields.id}, ${magicLinks.fields.id}
group by ${fields.id}
${conditionalSql(this.orderBy, ({ field, order }) => {
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
})}

View file

@ -138,7 +138,6 @@ describe('organization invitation creation', () => {
true
);
expect(invitation.organizationRoles.map((role) => role.id)).toEqual([role.id]);
expect(invitation.magicLink).toBeDefined();
// Test if get invitation by id works
const invitationById = await invitationApi.get(invitation.id);

View file

@ -0,0 +1,46 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table organization_invitations
drop column magic_link_id;
`);
await dropTableRls(pool, 'magic_links');
await pool.query(sql`
drop table magic_links;
`);
},
down: async (pool) => {
await pool.query(sql`
create table magic_links (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
/** The unique identifier of the link. */
id varchar(21) not null,
/** The token that can be used to verify the link. */
token varchar(32) not null,
/** The time when the link was created. */
created_at timestamptz not null default (now()),
/** The time when the link was consumed. */
consumed_at timestamptz,
primary key (id)
);
create index magic_links__token
on magic_links (tenant_id, token);
`);
await applyTableRls(pool, 'magic_links');
await pool.query(sql`
alter table organization_invitations
add column magic_link_id varchar(21)
references magic_links (id) on update cascade on delete cascade;
`);
},
};
export default alteration;

View file

@ -6,9 +6,7 @@ import {
type Organization,
Organizations,
type OrganizationInvitation,
type MagicLink,
OrganizationInvitations,
MagicLinks,
} from '../db-entries/index.js';
import { type UserInfo, type FeaturedUser, userInfoGuard } from './user.js';
@ -89,16 +87,13 @@ export type OrganizationWithFeatured = Organization & {
/**
* The organization invitation with additional fields:
*
* - `magicLink`: The magic link that can be used to accept the invitation.
* - `organizationRoles`: The roles to be assigned to the user when accepting the invitation.
*/
export type OrganizationInvitationEntity = OrganizationInvitation & {
magicLink?: MagicLink;
organizationRoles: OrganizationRoleEntity[];
};
export const organizationInvitationEntityGuard: z.ZodType<OrganizationInvitationEntity> =
OrganizationInvitations.guard.extend({
magicLink: MagicLinks.guard.optional(),
organizationRoles: organizationRoleEntityGuard.array(),
});

View file

@ -1,19 +0,0 @@
/* init_order = 1 */
/** Link that can be used to perform certain actions by verifying the token. The expiration time of the link should be determined by the action it performs, thus there is no `expires_at` column in this table. */
create table magic_links (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
/** The unique identifier of the link. */
id varchar(21) not null,
/** The token that can be used to verify the link. */
token varchar(32) not null,
/** The time when the link was created. */
created_at timestamptz not null default (now()),
/** The time when the link was consumed. */
consumed_at timestamptz,
primary key (id)
);
create index magic_links__token
on magic_links (tenant_id, token);

View file

@ -21,9 +21,6 @@ create table organization_invitations (
references organizations (id) on update cascade on delete cascade,
/** The status of the invitation. */
status organization_invitation_status not null,
/** The ID of the magic link that can be used to accept the invitation. */
magic_link_id varchar(21)
references magic_links (id) on update cascade on delete cascade,
/** The time when the invitation was created. */
created_at timestamptz not null default (now()),
/** The time when the invitation status was last updated. */