diff --git a/package-lock.json b/package-lock.json index c7c58f67..de1a4e9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/auth", - "version": "0.0.75", + "version": "0.0.76", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/auth", - "version": "0.0.75", + "version": "0.0.76", "license": "Apache-2.0", "dependencies": { "arctic": "^1.2.0", diff --git a/src/providers/Anonymous.ts b/src/providers/Anonymous.ts index b8323574..41c4f1a5 100644 --- a/src/providers/Anonymous.ts +++ b/src/providers/Anonymous.ts @@ -28,7 +28,13 @@ import { Value } from "convex/values"; /** * The available options to an {@link Anonymous} provider for Convex Auth. */ -export interface AnonymousConfig { +export interface AnonymousConfig< + DataModel extends GenericDataModel, + UserDocument extends Record = DocumentByName< + DataModel, + "users" + >, +> { /** * Uniquely identifies the provider, allowing to use * multiple different {@link Anonymous} providers. @@ -48,7 +54,7 @@ export interface AnonymousConfig { * the database. */ ctx: GenericActionCtxWithAuthConfig, - ) => WithoutSystemFields> & { + ) => WithoutSystemFields & { isAnonymous: true; }; } @@ -66,13 +72,13 @@ export function Anonymous( id: "anonymous", authorize: async (params, ctx) => { const profile = config.profile?.(params, ctx) ?? { isAnonymous: true }; - const { user } = await createAccount(ctx, { + const { account } = await createAccount(ctx, { provider, account: { id: crypto.randomUUID() }, profile: profile as any, }); // END - return { userId: user._id }; + return { userId: account.userId as string }; }, ...config, }); diff --git a/src/providers/ConvexCredentials.ts b/src/providers/ConvexCredentials.ts index 2f179c9a..439e7c90 100644 --- a/src/providers/ConvexCredentials.ts +++ b/src/providers/ConvexCredentials.ts @@ -60,7 +60,7 @@ export interface ConvexCredentialsUserConfig< credentials: Partial>, ctx: GenericActionCtxWithAuthConfig, ) => Promise<{ - userId: GenericId<"users">; + userId: string; sessionId?: GenericId<"authSessions">; } | null>; /** @@ -97,7 +97,7 @@ export interface ConvexCredentialsUserConfig< */ export function ConvexCredentials( config: ConvexCredentialsUserConfig, -): ConvexCredentialsConfig { +): ConvexCredentialsConfig { return { id: "credentials", type: "credentials", diff --git a/src/providers/Password.ts b/src/providers/Password.ts index d6defecd..bf1f5b2a 100644 --- a/src/providers/Password.ts +++ b/src/providers/Password.ts @@ -40,6 +40,7 @@ import { import { DocumentByName, GenericDataModel, + TableNamesInDataModel, WithoutSystemFields, } from "convex/server"; import { Value } from "convex/values"; @@ -48,7 +49,10 @@ import { Scrypt } from "lucia"; /** * The available options to a {@link Password} provider for Convex Auth. */ -export interface PasswordConfig { +export interface PasswordConfig< + DataModel extends GenericDataModel, + UsersTable extends TableNamesInDataModel = "users", +> { /** * Uniquely identifies the provider, allowing to use * multiple different {@link Password} providers. @@ -71,7 +75,7 @@ export interface PasswordConfig { * the database. */ ctx: GenericActionCtxWithAuthConfig, - ) => WithoutSystemFields> & { + ) => WithoutSystemFields> & { email: string; }; /** @@ -112,9 +116,10 @@ export interface PasswordConfig { * Email verification is not required unless you pass * an email provider to the `verify` option. */ -export function Password( - config: PasswordConfig = {}, -) { +export function Password< + DataModel extends GenericDataModel, + UsersTable extends TableNamesInDataModel = "users", +>(config: PasswordConfig = {}) { const provider = config.id ?? "password"; return ConvexCredentials({ id: "password", @@ -137,7 +142,6 @@ export function Password( const { email } = profile; const secret = params.password as string; let account: GenericDoc; - let user: GenericDoc; if (flow === "signUp") { if (secret === undefined) { throw new Error("Missing `password` param for `signUp` flow"); @@ -149,7 +153,7 @@ export function Password( shouldLinkViaEmail: config.verify !== undefined, shouldLinkViaPhone: false, }); - ({ account, user } = created); + ({ account } = created); } else if (flow === "signIn") { if (secret === undefined) { throw new Error("Missing `password` param for `signIn` flow"); @@ -161,7 +165,7 @@ export function Password( if (retrieved === null) { throw new Error("Invalid credentials"); } - ({ account, user } = retrieved); + ({ account } = retrieved); // START: Optional, support password reset } else if (flow === "reset") { if (!config.reset) { @@ -226,7 +230,7 @@ export function Password( }); } // END - return { userId: user._id }; + return { userId: account.userId as string }; }, crypto: { async hashSecret(password: string) { diff --git a/src/server/implementation/index.ts b/src/server/implementation/index.ts index 20a28578..11d1bdc0 100644 --- a/src/server/implementation/index.ts +++ b/src/server/implementation/index.ts @@ -10,6 +10,7 @@ import { queryGeneric, httpActionGeneric, internalMutationGeneric, + TableNamesInDataModel, } from "convex/server"; import { ConvexError, GenericId, Value, v } from "convex/values"; import { parse as parseCookies, serialize as serializeCookie } from "cookie"; @@ -49,7 +50,10 @@ import { import { signInImpl } from "./signIn.js"; import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js"; import { getAuthorizationUrl } from "../oauth/authorizationUrl.js"; -import { defaultCookiesOptions, oAuthConfigToInternalProvider } from "../oauth/convexAuth.js"; +import { + defaultCookiesOptions, + oAuthConfigToInternalProvider, +} from "../oauth/convexAuth.js"; import { handleOAuth } from "../oauth/callback.js"; export { getAuthSessionId } from "./sessions.js"; @@ -88,7 +92,9 @@ export type IsAuthenticatedQuery = FunctionReferenceFromExport< * @returns An object with fields you should reexport from your * `convex/auth.ts` file. */ -export function convexAuth(config_: ConvexAuthConfig) { +export function convexAuth>( + config_: ConvexAuthConfig, +) { const config = configDefaults(config_ as any); const hasOAuth = config.providers.some( (provider) => provider.type === "oauth" || provider.type === "oidc", @@ -135,7 +141,7 @@ export function convexAuth(config_: ConvexAuthConfig) { return null; } const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER); - return userId as GenericId<"users">; + return userId; }, /** * @deprecated - Use `getAuthSessionId` from "@convex-dev/auth/server": @@ -240,12 +246,10 @@ export function convexAuth(config_: ConvexAuthConfig) { providerId, ) as OAuthConfig; const { redirect, cookies, signature } = - await getAuthorizationUrl( - { - provider: await oAuthConfigToInternalProvider(provider), - cookies: defaultCookiesOptions(providerId), - }, - ); + await getAuthorizationUrl({ + provider: await oAuthConfigToInternalProvider(provider), + cookies: defaultCookiesOptions(providerId), + }); await callVerifierSignature(ctx, { verifier, @@ -295,10 +299,11 @@ export function convexAuth(config_: ConvexAuthConfig) { }); const params = url.searchParams; - + // Handle OAuth providers that use formData (such as Apple) if ( - request.headers.get("Content-Type") === "application/x-www-form-urlencoded" + request.headers.get("Content-Type") === + "application/x-www-form-urlencoded" ) { const formData = await request.formData(); for (const [key, value] of formData.entries()) { @@ -441,15 +446,18 @@ export function convexAuth(config_: ConvexAuthConfig) { return storeImpl(ctx, args, getProviderOrThrow, config); }, }), - + /** * Utility function for frameworks to use to get the current auth state * based on credentials that they've supplied separately. */ - isAuthenticated: queryGeneric({args: {}, handler: async (ctx, _args): Promise => { - const ident = await ctx.auth.getUserIdentity(); - return ident !== null; - }}), + isAuthenticated: queryGeneric({ + args: {}, + handler: async (ctx, _args): Promise => { + const ident = await ctx.auth.getUserIdentity(); + return ident !== null; + }, + }), }; } @@ -476,13 +484,15 @@ export function convexAuth(config_: ConvexAuthConfig) { * @param ctx query, mutation or action `ctx` * @returns the user ID or `null` if the client isn't authenticated */ -export async function getAuthUserId(ctx: { auth: Auth }) { +export async function getAuthUserId< + Id extends string = GenericId<"users">, +>(ctx: { auth: Auth }) { const identity = await ctx.auth.getUserIdentity(); if (identity === null) { return null; } const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER); - return userId as GenericId<"users">; + return userId as Id; } /** @@ -496,6 +506,7 @@ export async function getAuthUserId(ctx: { auth: Auth }) { */ export async function createAccount< DataModel extends GenericDataModel = GenericDataModel, + UserTable extends TableNamesInDataModel = "users", >( ctx: GenericActionCtx, args: { @@ -520,7 +531,7 @@ export async function createAccount< * The profile data to store for the user. * These must fit the `users` table schema. */ - profile: WithoutSystemFields>; + profile: WithoutSystemFields>; /** * If `true`, the account will be linked to an existing user * with the same verified email address. @@ -538,7 +549,6 @@ export async function createAccount< }, ): Promise<{ account: GenericDoc; - user: GenericDoc; }> { const actionCtx = ctx as unknown as ActionCtx; return await callCreateAccountFromCredentials(actionCtx, args); @@ -579,7 +589,6 @@ export async function retrieveAccount< }, ): Promise<{ account: GenericDoc; - user: GenericDoc; }> { const actionCtx = ctx as unknown as ActionCtx; const result = await callRetreiveAccountWithCredentials(actionCtx, args); @@ -629,7 +638,7 @@ export async function invalidateSessions< >( ctx: GenericActionCtx, args: { - userId: GenericId<"users">; + userId: string; except?: GenericId<"authSessions">[]; }, ): Promise { diff --git a/src/server/implementation/mutations/createAccountFromCredentials.ts b/src/server/implementation/mutations/createAccountFromCredentials.ts index 3d93a277..ae0d9f9c 100644 --- a/src/server/implementation/mutations/createAccountFromCredentials.ts +++ b/src/server/implementation/mutations/createAccountFromCredentials.ts @@ -14,7 +14,7 @@ export const createAccountFromCredentialsArgs = v.object({ shouldLinkViaPhone: v.optional(v.boolean()), }); -type ReturnType = { account: Doc<"authAccounts">; user: Doc<"users"> }; +type ReturnType = { account: Doc<"authAccounts"> }; export async function createAccountFromCredentialsImpl( ctx: MutationCtx, @@ -56,8 +56,6 @@ export async function createAccountFromCredentialsImpl( } return { account: existingAccount, - // TODO: Ian removed this, - user: (await ctx.db.get(existingAccount.userId))!, }; } @@ -65,7 +63,7 @@ export async function createAccountFromCredentialsImpl( account.secret !== undefined ? await Provider.hash(provider, account.secret) : undefined; - const { userId, accountId } = await upsertUserAndAccount( + const { accountId } = await upsertUserAndAccount( ctx, await getAuthSessionId(ctx), { providerAccountId: account.id, secret }, @@ -81,7 +79,6 @@ export async function createAccountFromCredentialsImpl( return { account: (await ctx.db.get(accountId))!, - user: (await ctx.db.get(userId))!, }; } diff --git a/src/server/implementation/mutations/invalidateSessions.ts b/src/server/implementation/mutations/invalidateSessions.ts index 32e37dd3..f0bcd994 100644 --- a/src/server/implementation/mutations/invalidateSessions.ts +++ b/src/server/implementation/mutations/invalidateSessions.ts @@ -4,7 +4,7 @@ import { ActionCtx, MutationCtx } from "../types.js"; import { LOG_LEVELS, logWithLevel } from "../utils.js"; export const invalidateSessionsArgs = v.object({ - userId: v.id("users"), + userId: v.string(), except: v.optional(v.array(v.id("authSessions"))), }); diff --git a/src/server/implementation/mutations/retrieveAccountWithCredentials.ts b/src/server/implementation/mutations/retrieveAccountWithCredentials.ts index 0e40af09..8f24b872 100644 --- a/src/server/implementation/mutations/retrieveAccountWithCredentials.ts +++ b/src/server/implementation/mutations/retrieveAccountWithCredentials.ts @@ -17,7 +17,7 @@ type ReturnType = | "InvalidAccountId" | "TooManyFailedAttempts" | "InvalidSecret" - | { account: Doc<"authAccounts">; user: Doc<"users"> }; + | { account: Doc<"authAccounts"> }; export async function retrieveAccountWithCredentialsImpl( ctx: MutationCtx, @@ -60,8 +60,6 @@ export async function retrieveAccountWithCredentialsImpl( } return { account: existingAccount, - // TODO: Ian removed this - user: (await ctx.db.get(existingAccount.userId))!, }; } diff --git a/src/server/implementation/mutations/signIn.ts b/src/server/implementation/mutations/signIn.ts index 3f4ee59b..7a925163 100644 --- a/src/server/implementation/mutations/signIn.ts +++ b/src/server/implementation/mutations/signIn.ts @@ -8,7 +8,7 @@ import { import { LOG_LEVELS, logWithLevel } from "../utils.js"; export const signInArgs = v.object({ - userId: v.id("users"), + userId: v.string(), sessionId: v.optional(v.id("authSessions")), generateTokens: v.boolean(), }); diff --git a/src/server/implementation/mutations/signOut.ts b/src/server/implementation/mutations/signOut.ts index 4b828d81..60d41bab 100644 --- a/src/server/implementation/mutations/signOut.ts +++ b/src/server/implementation/mutations/signOut.ts @@ -3,7 +3,7 @@ import { ActionCtx, MutationCtx } from "../types.js"; import { deleteSession, getAuthSessionId } from "../sessions.js"; type ReturnType = { - userId: GenericId<"users">; + userId: string; sessionId: GenericId<"authSessions">; } | null; diff --git a/src/server/implementation/sessions.ts b/src/server/implementation/sessions.ts index 3f4557b9..752ff1ee 100644 --- a/src/server/implementation/sessions.ts +++ b/src/server/implementation/sessions.ts @@ -21,7 +21,7 @@ const DEFAULT_SESSION_TOTAL_DURATION_MS = 1000 * 60 * 60 * 24 * 30; // 30 days export async function maybeGenerateTokensForSession( ctx: MutationCtx, config: ConvexAuthConfig, - userId: GenericId<"users">, + userId: string, sessionId: GenericId<"authSessions">, generateTokens: boolean, ): Promise { @@ -42,7 +42,7 @@ export async function maybeGenerateTokensForSession( export async function createNewAndDeleteExistingSession( ctx: MutationCtx, config: ConvexAuthConfig, - userId: GenericId<"users">, + userId: string, ) { const existingSessionId = await getAuthSessionId(ctx); if (existingSessionId !== null) { @@ -58,7 +58,7 @@ export async function generateTokensForSession( ctx: MutationCtx, config: ConvexAuthConfig, args: { - userId: GenericId<"users">; + userId: string; sessionId: GenericId<"authSessions">; issuedRefreshTokenId: GenericId<"authRefreshTokens"> | null; parentRefreshTokenId: GenericId<"authRefreshTokens"> | null; @@ -86,7 +86,7 @@ export async function generateTokensForSession( async function createSession( ctx: MutationCtx, - userId: GenericId<"users">, + userId: string, config: ConvexAuthConfig, ) { const expirationTime = diff --git a/src/server/implementation/tokens.ts b/src/server/implementation/tokens.ts index 3701f245..05556daa 100644 --- a/src/server/implementation/tokens.ts +++ b/src/server/implementation/tokens.ts @@ -8,7 +8,7 @@ const DEFAULT_JWT_DURATION_MS = 1000 * 60 * 60; // 1 hour export async function generateToken( args: { - userId: GenericId<"users">; + userId: string; sessionId: GenericId<"authSessions">; }, config: ConvexAuthConfig, diff --git a/src/server/implementation/types.ts b/src/server/implementation/types.ts index f6372045..1ff27553 100644 --- a/src/server/implementation/types.ts +++ b/src/server/implementation/types.ts @@ -54,7 +54,7 @@ export const authTables = { * See [Session document lifecycle](https://labs.convex.dev/auth/advanced#session-document-lifecycle). */ authSessions: defineTable({ - userId: v.id("users"), + userId: v.string(), expirationTime: v.number(), }).index("userId", ["userId"]), /** @@ -63,7 +63,7 @@ export const authTables = { * A single user can have multiple accounts linked. */ authAccounts: defineTable({ - userId: v.id("users"), + userId: v.string(), provider: v.string(), providerAccountId: v.string(), secret: v.optional(v.string()), @@ -144,12 +144,12 @@ export type Doc> = GenericDoc< export type Tokens = { token: string; refreshToken: string }; export type SessionInfo = { - userId: GenericId<"users">; + userId: string; sessionId: GenericId<"authSessions">; tokens: Tokens | null; }; export type SessionInfoWithTokens = { - userId: GenericId<"users">; + userId: string; sessionId: GenericId<"authSessions">; tokens: Tokens; }; diff --git a/src/server/implementation/users.ts b/src/server/implementation/users.ts index 0dc2e50c..548c9116 100644 --- a/src/server/implementation/users.ts +++ b/src/server/implementation/users.ts @@ -28,7 +28,7 @@ export async function upsertUserAndAccount( args: CreateOrUpdateUserArgs, config: ConvexAuthConfig, ): Promise<{ - userId: GenericId<"users">; + userId: string; accountId: GenericId<"authAccounts">; }> { const userId = await defaultCreateOrUpdateUser( @@ -54,13 +54,23 @@ async function defaultCreateOrUpdateUser( existingSessionId, args, }); - const existingUserId = existingAccount?.userId ?? null; + const existingUserId = (existingAccount?.userId ?? + null) as GenericId<"users"> | null; if (config.callbacks?.createOrUpdateUser !== undefined) { logWithLevel(LOG_LEVELS.DEBUG, "Using custom createOrUpdateUser callback"); return await config.callbacks.createOrUpdateUser(ctx, { existingUserId, ...args, }); + } else { + if ( + existingUserId && + ctx.db.normalizeId("users", existingUserId) === null + ) { + throw new Error( + `User ID \`${existingUserId}\` is not in the \`users\` table`, + ); + } } const { @@ -183,7 +193,7 @@ async function uniqueUserWithVerifiedPhone(ctx: QueryCtx, phone: string) { async function createOrUpdateAccount( ctx: MutationCtx, - userId: GenericId<"users">, + userId: string, account: | { existingAccount: Doc<"authAccounts"> } | { diff --git a/src/server/types.ts b/src/server/types.ts index 0b32c1a6..8acec4c9 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -19,7 +19,7 @@ import { GenericDoc } from "./convex_types.js"; /** * The config for the Convex Auth library, passed to `convexAuth`. */ -export type ConvexAuthConfig = { +export type ConvexAuthConfig> = { /** * A list of authentication provider configs. * @@ -136,7 +136,7 @@ export type ConvexAuthConfig = { * If this is a sign-in to an existing account, * this is the existing user ID linked to that account. */ - existingUserId: GenericId<"users"> | null; + existingUserId: UserId | null; /** * The provider type or "verification" if this callback is called * after an email or phone token verification. @@ -165,7 +165,7 @@ export type ConvexAuthConfig = { */ shouldLink?: boolean; }, - ) => Promise>; + ) => Promise; /** * Perform additional writes after a user is created. * @@ -186,12 +186,12 @@ export type ConvexAuthConfig = { /** * The ID of the user that is being signed in. */ - userId: GenericId<"users">; + userId: UserId; /** * If this is a sign-in to an existing account, * this is the existing user ID linked to that account. */ - existingUserId: GenericId<"users"> | null; + existingUserId: UserId | null; /** * The provider type or "verification" if this callback is called * after an email or phone token verification. @@ -341,7 +341,9 @@ export type PhoneUserConfig< /** * Similar to Auth.js Credentials config. */ -export type ConvexCredentialsConfig = ConvexCredentialsUserConfig & { +export type ConvexCredentialsConfig< + DataModel extends GenericDataModel = GenericDataModel, +> = ConvexCredentialsUserConfig & { type: "credentials"; id: string; }; diff --git a/test/convex/otp/TwilioSDK.ts b/test/convex/otp/TwilioSDK.ts index d681d1f8..ac3536fb 100644 --- a/test/convex/otp/TwilioSDK.ts +++ b/test/convex/otp/TwilioSDK.ts @@ -26,7 +26,7 @@ export const verify = internalAction({ console.error(status); throw new Error("Code could not be verified"); } - const { user } = await createAccount(ctx, { + const { account } = await createAccount(ctx, { provider: "twilio", account: { id: phone, @@ -36,7 +36,7 @@ export const verify = internalAction({ }, shouldLinkViaPhone: true, }); - return { userId: user._id }; + return { userId: account.userId }; }, });