From 3436d1821fb33c34c4c53a45edaa1cac83f28c64 Mon Sep 17 00:00:00 2001 From: Skyler Calaman <54462713+Blckbrry-Pi@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:47:09 -0400 Subject: [PATCH] refactor(auth_providers): split `providerData` into `uniqueData` and `additionalData` --- .../migration.sql | 9 ++- modules/auth_providers/db/schema.prisma | 14 +++- modules/auth_providers/module.json | 4 ++ .../scripts/add_provider_to_user.ts | 10 +-- .../get_or_create_user_from_provider.ts | 65 +++++++++++++++++++ .../scripts/get_provider_data.ts | 26 ++++++-- .../auth_providers/scripts/list_providers.ts | 24 +++---- .../scripts/set_provider_data.ts | 14 ++-- modules/auth_providers/utils/types.ts | 3 + 9 files changed, 139 insertions(+), 30 deletions(-) rename modules/auth_providers/db/migrations/{20240629231731_init => 20240701022604_init}/migration.sql (54%) create mode 100644 modules/auth_providers/scripts/get_or_create_user_from_provider.ts diff --git a/modules/auth_providers/db/migrations/20240629231731_init/migration.sql b/modules/auth_providers/db/migrations/20240701022604_init/migration.sql similarity index 54% rename from modules/auth_providers/db/migrations/20240629231731_init/migration.sql rename to modules/auth_providers/db/migrations/20240701022604_init/migration.sql index 1cc0d988..d13c476c 100644 --- a/modules/auth_providers/db/migrations/20240629231731_init/migration.sql +++ b/modules/auth_providers/db/migrations/20240701022604_init/migration.sql @@ -3,7 +3,8 @@ CREATE TABLE "ProviderEntries" ( "userId" UUID NOT NULL, "providerType" TEXT NOT NULL, "providerId" TEXT NOT NULL, - "providerData" JSONB NOT NULL, + "uniqueData" JSONB NOT NULL, + "additionalData" JSONB NOT NULL, CONSTRAINT "ProviderEntries_pkey" PRIMARY KEY ("userId","providerType","providerId") ); @@ -13,3 +14,9 @@ CREATE INDEX "ProviderEntries_userId_idx" ON "ProviderEntries"("userId"); -- CreateIndex CREATE INDEX "ProviderEntries_providerType_providerId_idx" ON "ProviderEntries"("providerType", "providerId"); + +-- CreateIndex +CREATE INDEX "ProviderEntries_providerType_providerId_uniqueData_idx" ON "ProviderEntries"("providerType", "providerId", "uniqueData"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProviderEntries_providerType_providerId_uniqueData_key" ON "ProviderEntries"("providerType", "providerId", "uniqueData"); diff --git a/modules/auth_providers/db/schema.prisma b/modules/auth_providers/db/schema.prisma index 0fb3c759..1d865078 100644 --- a/modules/auth_providers/db/schema.prisma +++ b/modules/auth_providers/db/schema.prisma @@ -6,11 +6,21 @@ datasource db { model ProviderEntries { userId String @db.Uuid + providerType String providerId String - providerData Json + uniqueData Json - @@id([userId, providerType, providerId]) + additionalData Json + + // Additional indexes for speed @@index([userId]) @@index([providerType, providerId]) + + // Each user should only have one identity per provider (for now) + @@id([userId, providerType, providerId]) + + // Each provider identity should only be linked to one user + @@index([providerType, providerId, uniqueData]) + @@unique([providerType, providerId, uniqueData]) } diff --git a/modules/auth_providers/module.json b/modules/auth_providers/module.json index 7d9fb6aa..ff4ed1a6 100644 --- a/modules/auth_providers/module.json +++ b/modules/auth_providers/module.json @@ -31,6 +31,10 @@ "name": "Set Provider Data", "description": "Set the data associated with a specific provider for a user." }, + "get_or_create_user_from_provider": { + "name": "Get or Create User From Provider", + "description": "Using provider info and data, match an existing user or create a new one based on the data." + }, "add_provider_to_user": { "name": "Add Provider To User", "description": "Add a new provider and its associated data to a user." diff --git a/modules/auth_providers/scripts/add_provider_to_user.ts b/modules/auth_providers/scripts/add_provider_to_user.ts index feb4e79d..b4b74a3d 100644 --- a/modules/auth_providers/scripts/add_provider_to_user.ts +++ b/modules/auth_providers/scripts/add_provider_to_user.ts @@ -1,10 +1,11 @@ -import { RuntimeError, ScriptContext, prisma } from "../module.gen.ts"; -import { ProviderData, ProviderInfo } from "../utils/types.ts"; +import { RuntimeError, ScriptContext } from "../module.gen.ts"; +import { ProviderDataInput, ProviderInfo } from "../utils/types.ts"; export interface Request { userToken: string; info: ProviderInfo; - data: ProviderData & prisma.Prisma.InputJsonValue; + uniqueData: ProviderDataInput; + additionalData: ProviderDataInput; } export interface Response { @@ -35,7 +36,8 @@ export async function run( userId, providerType: req.info.providerType, providerId: req.info.providerId, - providerData: req.data, + uniqueData: req.uniqueData, + additionalData: req.additionalData, }, }); diff --git a/modules/auth_providers/scripts/get_or_create_user_from_provider.ts b/modules/auth_providers/scripts/get_or_create_user_from_provider.ts new file mode 100644 index 00000000..94695aa2 --- /dev/null +++ b/modules/auth_providers/scripts/get_or_create_user_from_provider.ts @@ -0,0 +1,65 @@ +import { ScriptContext } from "../module.gen.ts"; +import { ProviderDataInput, ProviderInfo } from "../utils/types.ts"; + +export interface Request { + info: ProviderInfo; + uniqueData: ProviderDataInput; + additionalData: ProviderDataInput; + + suggestedUsername?: string; +} + +export interface Response { + userToken: string; +} + +export async function run( + ctx: ScriptContext, + req: Request, +): Promise { + const key = req.info.providerType + ":" + req.info.providerId + ":" + JSON.stringify(req.uniqueData); + await ctx.modules.rateLimit.throttle({ + key, + period: 10, + requests: 10, + type: "user", + }); + + // Get users the provider is associated with + const providers = await ctx.db.providerEntries.findFirst({ + where: { + providerType: req.info.providerType, + providerId: req.info.providerId, + uniqueData: { equals: req.uniqueData }, + }, + select: { + userId: true, + }, + }); + + // If the provider is associated with a user, generate a user token and + // return it + if (providers) { + const { token: { token } } = await ctx.modules.users.createToken({ userId: providers.userId }); + return { userToken: token }; + } + + // If the provider is not associated with a user, create a new user + const { user } = await ctx.modules.users.create({ username: req.suggestedUsername }); + + // Insert the provider data with the newly-created user + await ctx.db.providerEntries.create({ + data: { + userId: user.id, + providerType: req.info.providerType, + providerId: req.info.providerId, + uniqueData: req.uniqueData, + additionalData: req.additionalData, + }, + }); + + // Generate a user token and return it + const { token: { token } } = await ctx.modules.users.createToken({ userId: user.id }); + + return { userToken: token }; +} diff --git a/modules/auth_providers/scripts/get_provider_data.ts b/modules/auth_providers/scripts/get_provider_data.ts index 6f5c31a8..789391d3 100644 --- a/modules/auth_providers/scripts/get_provider_data.ts +++ b/modules/auth_providers/scripts/get_provider_data.ts @@ -7,7 +7,10 @@ export interface Request { } export interface Response { - data: ProviderData | null; + data: { + uniqueData: ProviderData; + additionalData: ProviderData; + } | null; } export async function run( @@ -21,8 +24,10 @@ export async function run( type: "user", }); + // Ensure the user token is valid and get the user ID const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } ); + // Get provider data const provider = await ctx.db.providerEntries.findFirst({ where: { userId, @@ -30,15 +35,24 @@ export async function run( providerId: req.info.providerId, }, select: { - providerData: true, + uniqueData: true, + additionalData: true } }); - const data = provider?.providerData; + // Type checking to make typescript happy + const data = provider ?? null; + if (!data) { + return { data: null }; + } - if (data && typeof data === 'object' && !Array.isArray(data)) { - return { data }; - } else { + const { uniqueData, additionalData } = data; + if (typeof uniqueData !== 'object' || Array.isArray(uniqueData) || uniqueData === null) { return { data: null }; } + if (typeof additionalData !== 'object' || Array.isArray(additionalData) || additionalData === null) { + return { data: null }; + } + + return { data: { uniqueData, additionalData } }; } diff --git a/modules/auth_providers/scripts/list_providers.ts b/modules/auth_providers/scripts/list_providers.ts index 969c6b61..3ee13f57 100644 --- a/modules/auth_providers/scripts/list_providers.ts +++ b/modules/auth_providers/scripts/list_providers.ts @@ -15,17 +15,19 @@ export async function run( ): Promise { await ctx.modules.rateLimit.throttlePublic({}); + // Ensure the user token is valid and get the user ID const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } ); - return { - providers: await ctx.db.providerEntries.findMany({ - where: { - userId, - }, - select: { - providerType: true, - providerId: true, - } - }), - }; + // Select providerType and providerId entries that match the userId + const providers = await ctx.db.providerEntries.findMany({ + where: { + userId, + }, + select: { + providerType: true, + providerId: true, + } + }); + + return { providers }; } diff --git a/modules/auth_providers/scripts/set_provider_data.ts b/modules/auth_providers/scripts/set_provider_data.ts index 66042c10..6aa2cfe3 100644 --- a/modules/auth_providers/scripts/set_provider_data.ts +++ b/modules/auth_providers/scripts/set_provider_data.ts @@ -1,10 +1,11 @@ -import { ScriptContext, Empty, RuntimeError, prisma } from "../module.gen.ts"; -import { ProviderData, ProviderInfo } from "../utils/types.ts"; +import { ScriptContext, Empty, RuntimeError } from "../module.gen.ts"; +import { ProviderDataInput, ProviderInfo } from "../utils/types.ts"; export interface Request { userToken: string; info: ProviderInfo; - data: ProviderData & prisma.Prisma.InputJsonValue; + uniqueData?: ProviderDataInput; + additionalData: ProviderDataInput; } export type Response = Empty; @@ -36,9 +37,10 @@ export async function run( providerId: req.info.providerId, } }, - data: { - providerData: req.data, - }, + data: req.uniqueData ? { + uniqueData: req.uniqueData, + additionalData: req.additionalData, + } : { additionalData: req.additionalData }, }); return {}; diff --git a/modules/auth_providers/utils/types.ts b/modules/auth_providers/utils/types.ts index 10a4aabd..a6799a6f 100644 --- a/modules/auth_providers/utils/types.ts +++ b/modules/auth_providers/utils/types.ts @@ -1,4 +1,7 @@ +import { prisma } from "../module.gen.ts"; + export type ProviderData = Record; +export type ProviderDataInput = ProviderData & prisma.Prisma.InputJsonValue; export interface ProviderInfo { providerType: string;