Update login stuff

This commit is contained in:
2025-08-29 16:41:24 -05:00
parent 6e892a8614
commit 1d32d31550
16 changed files with 1155 additions and 86 deletions

228
convex/CustomPassword.ts Normal file
View File

@@ -0,0 +1,228 @@
import {
ConvexCredentials,
type ConvexCredentialsUserConfig,
} from "@convex-dev/auth/providers/ConvexCredentials";
import {
type EmailConfig,
type GenericActionCtxWithAuthConfig,
type GenericDoc,
createAccount,
invalidateSessions,
modifyAccountCredentials,
retrieveAccount,
signInViaProvider,
} from "@convex-dev/auth/server";
import type {
DocumentByName,
GenericDataModel,
WithoutSystemFields,
} from "convex/server";
import type { Value } from "convex/values";
import { Scrypt } from "lucia";
export type PasswordConfig<DataModel extends GenericDataModel> = {
id?: string;
/**
* Perform checks on provided params and customize the user
* information stored after sign up, including email normalization.
*
* Called for every flow ("signUp", "signIn", "reset",
* "reset-verification" and "email-verification").
*/
profile?: (
/**
* The values passed to the `signIn` function.
*/
params: Record<string, Value | undefined>,
/**
* Convex ActionCtx in case you want to read from or write to
* the database.
*/
ctx: GenericActionCtxWithAuthConfig<DataModel>,
) => WithoutSystemFields<DocumentByName<DataModel, "users">> & {
email: string;
};
/**
* Performs custom validation on password provided during sign up or reset.
*
* Otherwise the default validation is used (password is not empty and
* at least 8 characters in length).
*
* If the provided password is invalid, implementations must throw an Error.
*
* @param password the password supplied during "signUp" or
* "reset-verification" flows.
*/
validatePasswordRequirements?: (password: string) => void;
/**
* Provide hashing and verification functions if you want to control
* how passwords are hashed.
*/
crypto?: ConvexCredentialsUserConfig["crypto"];
/**
* An Auth.js email provider used to require verification
* before password reset.
*/
reset?: EmailConfig | ((...args: any) => EmailConfig);
/**
* An Auth.js email provider used to require verification
* before sign up / sign in.
*/
verify?: EmailConfig | ((...args: any) => EmailConfig);
}
/**
* Email and password authentication provider.
*
* Passwords are by default hashed using Scrypt from Lucia.
* You can customize the hashing via the `crypto` option.
*
* Email verification is not required unless you pass
* an email provider to the `verify` option.
*/
export function Password<DataModel extends GenericDataModel>(
config: PasswordConfig<DataModel> = {},
) {
const provider = config.id ?? "password";
return ConvexCredentials<DataModel>({
id: "password",
authorize: async (params, ctx) => {
const flow = params.flow as string;
const passwordToValidate =
flow === "signUp"
? (params.password as string)
: flow === "reset-verification"
? (params.newPassword as string)
: null;
if (passwordToValidate !== null) {
if (config.validatePasswordRequirements !== undefined) {
config.validatePasswordRequirements(passwordToValidate);
} else {
validateDefaultPasswordRequirements(passwordToValidate);
}
}
const profile = config.profile?.(params, ctx) ?? defaultProfile(params);
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");
}
const created = await createAccount(ctx, {
provider,
account: { id: email, secret },
profile: profile as any,
shouldLinkViaEmail: config.verify !== undefined,
shouldLinkViaPhone: false,
});
({ account, user } = created);
} else if (flow === "signIn") {
if (secret === undefined) {
throw new Error("Missing `password` param for `signIn` flow");
}
const retrieved = await retrieveAccount(ctx, {
provider,
account: { id: email, secret },
});
if (retrieved === null) {
throw new Error("Invalid credentials");
}
({ account, user } = retrieved);
// START: Optional, support password reset
} else if (flow === "reset") {
if (!config.reset) {
throw new Error(`Password reset is not enabled for ${provider}`);
}
const { account } = await retrieveAccount(ctx, {
provider,
account: { id: email },
});
return await signInViaProvider(ctx, config.reset, {
accountId: account._id,
params,
});
} else if (flow === "reset-verification") {
if (!config.reset) {
throw new Error(`Password reset is not enabled for ${provider}`);
}
if (params.newPassword === undefined) {
throw new Error(
"Missing `newPassword` param for `reset-verification` flow",
);
}
const result = await signInViaProvider(ctx, config.reset, { params });
if (result === null) {
throw new Error("Invalid code");
}
const { userId, sessionId } = result;
const secret = params.newPassword as string;
await modifyAccountCredentials(ctx, {
provider,
account: { id: email, secret },
});
await invalidateSessions(ctx, { userId, except: [sessionId] });
return { userId, sessionId };
// END
// START: Optional, email verification during sign in
} else if (flow === "email-verification") {
if (!config.verify) {
throw new Error(`Email verification is not enabled for ${provider}`);
}
const { account } = await retrieveAccount(ctx, {
provider,
account: { id: email },
});
return await signInViaProvider(ctx, config.verify, {
accountId: account._id,
params,
});
// END
} else {
throw new Error(
"Missing `flow` param, it must be one of " +
'"signUp", "signIn", "reset", "reset-verification" or ' +
'"email-verification"!',
);
}
// START: Optional, email verification during sign in
if (config.verify && !account.emailVerified) {
return await signInViaProvider(ctx, config.verify, {
accountId: account._id,
params,
});
}
// END
return { userId: user._id };
},
crypto: {
async hashSecret(password: string) {
return await new Scrypt().hash(password);
},
async verifySecret(password: string, hash: string) {
return await new Scrypt().verify(hash, password);
},
},
extraProviders: [config.reset, config.verify],
...config,
});
}
function validateDefaultPasswordRequirements(password: string) {
if (
password.length < 8 ||
!/\d/.test(password) ||
!/[a-z]/.test(password) ||
!/[A-Z]/.test(password)
) {
throw new Error("Invalid password.");
}
}
function defaultProfile(params: Record<string, unknown>) {
return {
email: params.email as string,
name: params.name as string,
};
}

View File

@@ -13,6 +13,7 @@ import type {
FilterApi,
FunctionReference,
} from "convex/server";
import type * as CustomPassword from "../CustomPassword.js";
import type * as auth from "../auth.js";
import type * as http from "../http.js";
import type * as myFunctions from "../myFunctions.js";
@@ -26,6 +27,7 @@ import type * as myFunctions from "../myFunctions.js";
* ```
*/
declare const fullApi: ApiFromModules<{
CustomPassword: typeof CustomPassword;
auth: typeof auth;
http: typeof http;
myFunctions: typeof myFunctions;

View File

@@ -1,5 +1,5 @@
import { Password } from '@convex-dev/auth/providers/Password';
import { convexAuth } from '@convex-dev/auth/server';
import { Password } from './CustomPassword';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],