Skip to content

downgrade userId #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions src/providers/Anonymous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ import { Value } from "convex/values";
/**
* The available options to an {@link Anonymous} provider for Convex Auth.
*/
export interface AnonymousConfig<DataModel extends GenericDataModel> {
export interface AnonymousConfig<
DataModel extends GenericDataModel,
UserDocument extends Record<string, Value> = DocumentByName<
DataModel,
"users"
>,
Comment on lines +33 to +36
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just have this be the same as other providers, vs. returning a specific type.. not sure if this is causing the type issues

> {
/**
* Uniquely identifies the provider, allowing to use
* multiple different {@link Anonymous} providers.
Expand All @@ -48,7 +54,7 @@ export interface AnonymousConfig<DataModel extends GenericDataModel> {
* the database.
*/
ctx: GenericActionCtxWithAuthConfig<DataModel>,
) => WithoutSystemFields<DocumentByName<DataModel, "users">> & {
) => WithoutSystemFields<UserDocument> & {
isAnonymous: true;
};
}
Expand All @@ -66,13 +72,13 @@ export function Anonymous<DataModel extends GenericDataModel>(
id: "anonymous",
authorize: async (params, ctx) => {
const profile = config.profile?.(params, ctx) ?? { isAnonymous: true };
const { user } = await createAccount(ctx, {
const { account } = await createAccount(ctx, {
Comment on lines -69 to +75
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one concern is that folks who have made their own providers may be tripped up by createAccount not returning the user anymore. This would be a breaking change technically

provider,
account: { id: crypto.randomUUID() },
profile: profile as any,
});
// END
return { userId: user._id };
return { userId: account.userId as string };
},
...config,
});
Expand Down
4 changes: 2 additions & 2 deletions src/providers/ConvexCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface ConvexCredentialsUserConfig<
credentials: Partial<Record<string, Value | undefined>>,
ctx: GenericActionCtxWithAuthConfig<DataModel>,
) => Promise<{
userId: GenericId<"users">;
userId: string;
sessionId?: GenericId<"authSessions">;
} | null>;
/**
Expand Down Expand Up @@ -97,7 +97,7 @@ export interface ConvexCredentialsUserConfig<
*/
export function ConvexCredentials<DataModel extends GenericDataModel>(
config: ConvexCredentialsUserConfig<DataModel>,
): ConvexCredentialsConfig {
): ConvexCredentialsConfig<DataModel> {
return {
id: "credentials",
type: "credentials",
Expand Down
22 changes: 13 additions & 9 deletions src/providers/Password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import {
DocumentByName,
GenericDataModel,
TableNamesInDataModel,
WithoutSystemFields,
} from "convex/server";
import { Value } from "convex/values";
Expand All @@ -48,7 +49,10 @@ import { Scrypt } from "lucia";
/**
* The available options to a {@link Password} provider for Convex Auth.
*/
export interface PasswordConfig<DataModel extends GenericDataModel> {
export interface PasswordConfig<
DataModel extends GenericDataModel,
UsersTable extends TableNamesInDataModel<DataModel> = "users",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could just do the UsersDoc like anonymous to be more flexible, but not sure which is more convenient / types better

> {
/**
* Uniquely identifies the provider, allowing to use
* multiple different {@link Password} providers.
Expand All @@ -71,7 +75,7 @@ export interface PasswordConfig<DataModel extends GenericDataModel> {
* the database.
*/
ctx: GenericActionCtxWithAuthConfig<DataModel>,
) => WithoutSystemFields<DocumentByName<DataModel, "users">> & {
) => WithoutSystemFields<DocumentByName<DataModel, UsersTable>> & {
email: string;
};
/**
Expand Down Expand Up @@ -112,9 +116,10 @@ export interface PasswordConfig<DataModel extends GenericDataModel> {
* Email verification is not required unless you pass
* an email provider to the `verify` option.
*/
export function Password<DataModel extends GenericDataModel>(
config: PasswordConfig<DataModel> = {},
) {
export function Password<
DataModel extends GenericDataModel,
UsersTable extends TableNamesInDataModel<DataModel> = "users",
>(config: PasswordConfig<DataModel, UsersTable> = {}) {
const provider = config.id ?? "password";
return ConvexCredentials<DataModel>({
id: "password",
Expand All @@ -137,7 +142,6 @@ export function Password<DataModel extends GenericDataModel>(
const { email } = profile;
const secret = params.password as string;
let account: GenericDoc<DataModel, "authAccounts">;
let user: GenericDoc<DataModel, "users">;
if (flow === "signUp") {
if (secret === undefined) {
throw new Error("Missing `password` param for `signUp` flow");
Expand All @@ -149,7 +153,7 @@ export function Password<DataModel extends GenericDataModel>(
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");
Expand All @@ -161,7 +165,7 @@ export function Password<DataModel extends GenericDataModel>(
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) {
Expand Down Expand Up @@ -226,7 +230,7 @@ export function Password<DataModel extends GenericDataModel>(
});
}
// END
return { userId: user._id };
return { userId: account.userId as string };
},
crypto: {
async hashSecret(password: string) {
Expand Down
53 changes: 31 additions & 22 deletions src/server/implementation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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<UserId extends string = GenericId<"users">>(
config_: ConvexAuthConfig<UserId>,
) {
const config = configDefaults(config_ as any);
const hasOAuth = config.providers.some(
(provider) => provider.type === "oauth" || provider.type === "oidc",
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -240,12 +246,10 @@ export function convexAuth(config_: ConvexAuthConfig) {
providerId,
) as OAuthConfig<any>;
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,
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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<boolean> => {
const ident = await ctx.auth.getUserIdentity();
return ident !== null;
}}),
isAuthenticated: queryGeneric({
args: {},
handler: async (ctx, _args): Promise<boolean> => {
const ident = await ctx.auth.getUserIdentity();
return ident !== null;
},
}),
};
}

Expand All @@ -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;
}

/**
Expand All @@ -496,6 +506,7 @@ export async function getAuthUserId(ctx: { auth: Auth }) {
*/
export async function createAccount<
DataModel extends GenericDataModel = GenericDataModel,
UserTable extends TableNamesInDataModel<DataModel> = "users",
>(
ctx: GenericActionCtx<DataModel>,
args: {
Expand All @@ -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<DocumentByName<DataModel, "users">>;
profile: WithoutSystemFields<DocumentByName<DataModel, UserTable>>;
/**
* If `true`, the account will be linked to an existing user
* with the same verified email address.
Expand All @@ -538,7 +549,6 @@ export async function createAccount<
},
): Promise<{
account: GenericDoc<DataModel, "authAccounts">;
user: GenericDoc<DataModel, "users">;
}> {
const actionCtx = ctx as unknown as ActionCtx;
return await callCreateAccountFromCredentials(actionCtx, args);
Expand Down Expand Up @@ -579,7 +589,6 @@ export async function retrieveAccount<
},
): Promise<{
account: GenericDoc<DataModel, "authAccounts">;
user: GenericDoc<DataModel, "users">;
}> {
const actionCtx = ctx as unknown as ActionCtx;
const result = await callRetreiveAccountWithCredentials(actionCtx, args);
Expand Down Expand Up @@ -629,7 +638,7 @@ export async function invalidateSessions<
>(
ctx: GenericActionCtx<DataModel>,
args: {
userId: GenericId<"users">;
userId: string;
except?: GenericId<"authSessions">[];
},
): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,16 +56,14 @@ export async function createAccountFromCredentialsImpl(
}
return {
account: existingAccount,
// TODO: Ian removed this,
user: (await ctx.db.get(existingAccount.userId))!,
};
}

const secret =
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 },
Expand All @@ -81,7 +79,6 @@ export async function createAccountFromCredentialsImpl(

return {
account: (await ctx.db.get(accountId))!,
user: (await ctx.db.get(userId))!,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/server/implementation/mutations/invalidateSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type ReturnType =
| "InvalidAccountId"
| "TooManyFailedAttempts"
| "InvalidSecret"
| { account: Doc<"authAccounts">; user: Doc<"users"> };
| { account: Doc<"authAccounts"> };

export async function retrieveAccountWithCredentialsImpl(
ctx: MutationCtx,
Expand Down Expand Up @@ -60,8 +60,6 @@ export async function retrieveAccountWithCredentialsImpl(
}
return {
account: existingAccount,
// TODO: Ian removed this
user: (await ctx.db.get(existingAccount.userId))!,
Comment on lines -63 to -64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember why this was added back. I thought it wasn't needed beyond getting the userId, but there's a decent amount of untyped code..

};
}

Expand Down
2 changes: 1 addition & 1 deletion src/server/implementation/mutations/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand Down
2 changes: 1 addition & 1 deletion src/server/implementation/mutations/signOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading
Loading