mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-25 10:13:54 -05:00
feat: error codes (#771)
This commit is contained in:
@@ -4,6 +4,7 @@ import { socialProviderList } from "../../social-providers";
|
||||
import { APIError } from "better-call";
|
||||
import { generateState } from "../../oauth2";
|
||||
import { sessionMiddleware } from "./session";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const listUserAccounts = createAuthEndpoint(
|
||||
"/list-accounts",
|
||||
@@ -123,7 +124,7 @@ export const linkSocialAccount = createAuthEndpoint(
|
||||
);
|
||||
if (existingAccount) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Social Account is already linked.",
|
||||
message: BASE_ERROR_CODES.SOCIAL_ACCOUNT_ALREADY_LINKED,
|
||||
});
|
||||
}
|
||||
const provider = c.context.socialProviders.find(
|
||||
@@ -137,7 +138,7 @@ export const linkSocialAccount = createAuthEndpoint(
|
||||
},
|
||||
);
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Provider not found",
|
||||
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const state = await generateState(c, {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { APIError } from "better-call";
|
||||
import { getSessionFromCtx } from "./session";
|
||||
import { setSessionCookie } from "../../cookies";
|
||||
import type { GenericEndpointContext, User } from "../../types";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export async function createEmailVerificationToken(
|
||||
secret: string,
|
||||
@@ -143,7 +144,7 @@ export const sendVerificationEmail = createAuthEndpoint(
|
||||
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found",
|
||||
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
await sendVerificationEmailFn(ctx, user.user);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { APIError } from "better-call";
|
||||
import type { AuthContext } from "../../init";
|
||||
import { getDate } from "../../utils/date";
|
||||
import { generateId } from "../../utils";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
function redirectError(
|
||||
ctx: AuthContext,
|
||||
@@ -241,7 +242,7 @@ export const resetPassword = createAuthEndpoint(
|
||||
: "");
|
||||
if (!token) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Token not found",
|
||||
message: BASE_ERROR_CODES.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,7 +253,7 @@ export const resetPassword = createAuthEndpoint(
|
||||
await ctx.context.internalAdapter.findVerificationValue(id);
|
||||
if (!verification || verification.expiresAt < new Date()) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid token",
|
||||
message: BASE_ERROR_CODES.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "../../types";
|
||||
import { hmac } from "../../crypto/hash";
|
||||
import { safeJSONParse } from "../../utils/json";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const getSession = <Option extends BetterAuthOptions>() =>
|
||||
createAuthEndpoint(
|
||||
@@ -238,7 +239,7 @@ export const getSession = <Option extends BetterAuthOptions>() =>
|
||||
} catch (error) {
|
||||
ctx.context.logger.error("INTERNAL_SERVER_ERROR", error);
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "internal server error",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { socialProviderList } from "../../social-providers";
|
||||
import { createEmailVerificationToken } from "./email-verification";
|
||||
import { generateState } from "../../utils";
|
||||
import { handleOAuthUserInfo } from "../../oauth2/link-account";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const signInSocial = createAuthEndpoint(
|
||||
"/sign-in/social",
|
||||
@@ -163,7 +164,7 @@ export const signInSocial = createAuthEndpoint(
|
||||
},
|
||||
);
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Provider not found",
|
||||
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ export const signInSocial = createAuthEndpoint(
|
||||
},
|
||||
);
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Provider does not support id token verification",
|
||||
message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
|
||||
});
|
||||
}
|
||||
const { token, nonce } = c.body.idToken;
|
||||
@@ -186,7 +187,7 @@ export const signInSocial = createAuthEndpoint(
|
||||
provider: c.body.provider,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid id token",
|
||||
message: BASE_ERROR_CODES.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
const userInfo = await provider.getUserInfo({
|
||||
@@ -199,7 +200,7 @@ export const signInSocial = createAuthEndpoint(
|
||||
provider: c.body.provider,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Failed to get user info",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
|
||||
});
|
||||
}
|
||||
if (!userInfo.user.email) {
|
||||
@@ -207,7 +208,7 @@ export const signInSocial = createAuthEndpoint(
|
||||
provider: c.body.provider,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "User email not found",
|
||||
message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const data = await handleOAuthUserInfo(c, {
|
||||
@@ -337,7 +338,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
const isValidEmail = z.string().email().safeParse(email);
|
||||
if (!isValidEmail.success) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid email",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL,
|
||||
});
|
||||
}
|
||||
const user = await ctx.context.internalAdapter.findUserByEmail(email, {
|
||||
@@ -348,7 +349,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
await ctx.context.password.hash(password);
|
||||
ctx.context.logger.error("User not found", { email });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid email or password",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -358,14 +359,14 @@ export const signInEmail = createAuthEndpoint(
|
||||
if (!credentialAccount) {
|
||||
ctx.context.logger.error("Credential account not found", { email });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid email or password",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const currentPassword = credentialAccount?.password;
|
||||
if (!currentPassword) {
|
||||
ctx.context.logger.error("Password not found", { email });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Unexpected error",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const validPassword = await ctx.context.password.verify({
|
||||
@@ -375,7 +376,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
if (!validPassword) {
|
||||
ctx.context.logger.error("Invalid password");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid email or password",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,7 +386,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
) {
|
||||
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Email is not verified.",
|
||||
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
||||
});
|
||||
}
|
||||
const token = await createEmailVerificationToken(
|
||||
@@ -405,8 +406,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
);
|
||||
ctx.context.logger.error("Email not verified", { email });
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message:
|
||||
"Email is not verified. Check your email for a verification link",
|
||||
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ export const signInEmail = createAuthEndpoint(
|
||||
if (!session) {
|
||||
ctx.context.logger.error("Failed to create session");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Failed to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
import { deleteSessionCookie } from "../../cookies";
|
||||
import { APIError } from "better-call";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const signOut = createAuthEndpoint(
|
||||
"/sign-out",
|
||||
@@ -39,7 +40,7 @@ export const signOut = createAuthEndpoint(
|
||||
if (!sessionCookieToken) {
|
||||
deleteSessionCookie(ctx);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Session not found",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
|
||||
});
|
||||
}
|
||||
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from "../../types";
|
||||
import type { toZod } from "../../types/to-zod";
|
||||
import { parseUserInput } from "../../db/schema";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
createAuthEndpoint(
|
||||
@@ -102,13 +103,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
|
||||
if (!isValidEmail.success) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid email",
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof password !== "string") {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password must be a string",
|
||||
message: BASE_ERROR_CODES.INVALID_EMAIL,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +111,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
if (password.length < minPasswordLength) {
|
||||
ctx.context.logger.error("Password is too short");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password is too short",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,14 +119,14 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
if (password.length > maxPasswordLength) {
|
||||
ctx.context.logger.error("Password is too long");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password is too long",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
|
||||
});
|
||||
}
|
||||
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
||||
if (dbUser?.user) {
|
||||
ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`);
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: "User with this email already exists",
|
||||
message: BASE_ERROR_CODES.USER_ALREADY_EXISTS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,21 +134,30 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
ctx.context.options,
|
||||
additionalFields as any,
|
||||
);
|
||||
const createdUser = await ctx.context.internalAdapter.createUser({
|
||||
email: email.toLowerCase(),
|
||||
name,
|
||||
image,
|
||||
...additionalData,
|
||||
emailVerified: false,
|
||||
});
|
||||
if (!createdUser) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Failed to create user",
|
||||
let createdUser: User;
|
||||
try {
|
||||
createdUser = await ctx.context.internalAdapter.createUser({
|
||||
email: email.toLowerCase(),
|
||||
name,
|
||||
image,
|
||||
...additionalData,
|
||||
emailVerified: false,
|
||||
});
|
||||
if (!createdUser) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.context.logger.error("Failed to create user", e);
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
details: e,
|
||||
});
|
||||
}
|
||||
if (!createdUser) {
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: "Failed to create user",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -202,7 +206,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
);
|
||||
if (!session) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Failed to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
|
||||
});
|
||||
}
|
||||
await setSessionCookie(ctx, {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { toZod } from "../../types/to-zod";
|
||||
import type { AdditionalUserFieldsInput, BetterAuthOptions } from "../../types";
|
||||
import { parseUserInput } from "../../db/schema";
|
||||
import { alphabet, generateRandomString } from "../../crypto";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const updateUser = <O extends BetterAuthOptions>() =>
|
||||
createAuthEndpoint(
|
||||
@@ -78,7 +79,7 @@ export const updateUser = <O extends BetterAuthOptions>() =>
|
||||
|
||||
if (body.email) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "You can't update email",
|
||||
message: BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED,
|
||||
});
|
||||
}
|
||||
const { name, image, ...rest } = body;
|
||||
@@ -173,7 +174,7 @@ export const changePassword = createAuthEndpoint(
|
||||
if (newPassword.length < minPasswordLength) {
|
||||
ctx.context.logger.error("Password is too short");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password is too short",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ export const changePassword = createAuthEndpoint(
|
||||
if (newPassword.length > maxPasswordLength) {
|
||||
ctx.context.logger.error("Password is too long");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password too long",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ export const changePassword = createAuthEndpoint(
|
||||
);
|
||||
if (!account || !account.password) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User does not have a password",
|
||||
message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const passwordHash = await ctx.context.password.hash(newPassword);
|
||||
@@ -204,7 +205,7 @@ export const changePassword = createAuthEndpoint(
|
||||
});
|
||||
if (!verify) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Incorrect password",
|
||||
message: BASE_ERROR_CODES.INVALID_PASSWORD,
|
||||
});
|
||||
}
|
||||
await ctx.context.internalAdapter.updateAccount(account.id, {
|
||||
@@ -218,7 +219,7 @@ export const changePassword = createAuthEndpoint(
|
||||
);
|
||||
if (!newSession) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Unable to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
|
||||
});
|
||||
}
|
||||
// set the new session cookie
|
||||
@@ -254,7 +255,7 @@ export const setPassword = createAuthEndpoint(
|
||||
if (newPassword.length < minPasswordLength) {
|
||||
ctx.context.logger.error("Password is too short");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password is too short",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,7 +264,7 @@ export const setPassword = createAuthEndpoint(
|
||||
if (newPassword.length > maxPasswordLength) {
|
||||
ctx.context.logger.error("Password is too long");
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Password too long",
|
||||
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,7 +383,7 @@ export const deleteUserCallback = createAuthEndpoint(
|
||||
const session = await getSessionFromCtx(ctx);
|
||||
if (!session) {
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "No session found",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
|
||||
});
|
||||
}
|
||||
const token = await ctx.context.internalAdapter.findVerificationValue(
|
||||
@@ -393,12 +394,12 @@ export const deleteUserCallback = createAuthEndpoint(
|
||||
await ctx.context.internalAdapter.deleteVerificationValue(token.id);
|
||||
}
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Invalid token",
|
||||
message: BASE_ERROR_CODES.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
if (token.value !== session.user.id) {
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Invalid token",
|
||||
message: BASE_ERROR_CODES.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getEndpoints, router } from "./api";
|
||||
import { init } from "./init";
|
||||
import type { BetterAuthOptions } from "./types/options";
|
||||
import type {
|
||||
InferPluginErrorCodes,
|
||||
InferPluginTypes,
|
||||
InferSession,
|
||||
InferUser,
|
||||
@@ -9,11 +10,20 @@ import type {
|
||||
} from "./types";
|
||||
import { getBaseURL } from "./utils/url";
|
||||
import type { FilterActions, InferAPI } from "./types/api";
|
||||
import { BASE_ERROR_CODES } from "./error/codes";
|
||||
|
||||
export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
|
||||
const authContext = init(options);
|
||||
const { api } = getEndpoints(authContext, options);
|
||||
|
||||
const errorCodes = options.plugins?.reduce((acc, plugin) => {
|
||||
if (plugin.$ERROR_CODES) {
|
||||
return {
|
||||
...acc,
|
||||
...plugin.$ERROR_CODES,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return {
|
||||
handler: async (request: Request) => {
|
||||
const ctx = await authContext;
|
||||
@@ -42,6 +52,10 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
|
||||
user: PrettifyDeep<InferUser<O>>;
|
||||
};
|
||||
} & InferPluginTypes<O>,
|
||||
$ErrorCodes: {
|
||||
...errorCodes,
|
||||
...BASE_ERROR_CODES,
|
||||
} as InferPluginErrorCodes<O> & typeof BASE_ERROR_CODES,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,4 +63,5 @@ export type Auth = {
|
||||
handler: (request: Request) => Promise<Response>;
|
||||
api: FilterActions<ReturnType<typeof router>["endpoints"]>;
|
||||
options: BetterAuthOptions;
|
||||
$ERROR_CODES: typeof BASE_ERROR_CODES;
|
||||
};
|
||||
|
||||
@@ -4,17 +4,19 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferErrorCodes,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { UnionToIntersection } from "../../types/helper";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import { useStore } from "./react-store";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
@@ -92,6 +94,9 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
InferErrorCodes,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import type { Accessor } from "solid-js";
|
||||
import type { UnionToIntersection } from "../../types/helper";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import { useStore } from "./solid-store";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
@@ -84,5 +84,8 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferErrorCodes,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { UnionToIntersection } from "../../types/helper";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type { Atom } from "nanostores";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
infer Plugin
|
||||
@@ -83,5 +85,8 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
$Infer: {
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +97,17 @@ export type InferActions<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
|
||||
export type InferErrorCodes<O extends ClientOptions> =
|
||||
O["plugins"] extends Array<infer Plugin>
|
||||
? UnionToIntersection<
|
||||
Plugin extends BetterAuthClientPlugin
|
||||
? Plugin["$InferServerPlugin"] extends BetterAuthPlugin
|
||||
? Plugin["$InferServerPlugin"]["$ERROR_CODES"]
|
||||
: {}
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
/**
|
||||
* signals are just used to recall a computed value.
|
||||
* as a convention they start with "$"
|
||||
|
||||
@@ -5,17 +5,17 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
InferErrorCodes,
|
||||
IsSignal,
|
||||
} from "./types";
|
||||
import { createDynamicPathProxy } from "./proxy";
|
||||
import type { UnionToIntersection } from "../types/helper";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../types/helper";
|
||||
import type { Atom } from "nanostores";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { BASE_ERROR_CODES } from "../error/codes";
|
||||
|
||||
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
infer Plugin
|
||||
@@ -82,5 +82,8 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
$Infer: {
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferErrorCodes,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { UnionToIntersection } from "../../types/helper";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
@@ -125,5 +127,8 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const BASE_ERROR_CODES = {
|
||||
USER_NOT_FOUND: "User not found",
|
||||
FAILED_TO_CREATE_USER: "Failed to create user",
|
||||
FAILED_TO_CREATE_SESSION: "Failed to create session",
|
||||
FAILED_TO_UPDATE_USER: "Failed to update user",
|
||||
FAILED_TO_GET_SESSION: "Failed to get session",
|
||||
INVALID_PASSWORD: "Invalid password",
|
||||
INVALID_EMAIL: "Invalid email",
|
||||
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
|
||||
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
|
||||
PROVIDER_NOT_FOUND: "Provider not found",
|
||||
INVALID_TOKEN: "invalid token",
|
||||
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
|
||||
FAILED_TO_GET_USER_INFO: "Failed to get user info",
|
||||
USER_EMAIL_NOT_FOUND: "User email not found",
|
||||
EMAIL_NOT_VERIFIED: "Email not verified",
|
||||
PASSWORD_TOO_SHORT: "Password too short",
|
||||
PASSWORD_TOO_LONG: "Password too long",
|
||||
USER_ALREADY_EXISTS: "User already exists",
|
||||
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
|
||||
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
|
||||
};
|
||||
@@ -4,36 +4,35 @@ import { admin, type UserWithRole } from ".";
|
||||
import { adminClient } from "./client";
|
||||
|
||||
describe("Admin plugin", async () => {
|
||||
const { client, auth, signInWithTestUser, signInWithUser } =
|
||||
await getTestInstance(
|
||||
{
|
||||
plugins: [admin()],
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (user) => {
|
||||
if (user.name === "Admin") {
|
||||
return {
|
||||
data: {
|
||||
...user,
|
||||
role: "admin",
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
const { client, signInWithTestUser, signInWithUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [admin()],
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (user) => {
|
||||
if (user.name === "Admin") {
|
||||
return {
|
||||
data: {
|
||||
...user,
|
||||
role: "admin",
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testUser: {
|
||||
name: "Admin",
|
||||
},
|
||||
clientOptions: {
|
||||
plugins: [adminClient()],
|
||||
},
|
||||
},
|
||||
{
|
||||
testUser: {
|
||||
name: "Admin",
|
||||
},
|
||||
);
|
||||
clientOptions: {
|
||||
plugins: [adminClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
const { headers: adminHeaders } = await signInWithTestUser();
|
||||
let newUser: UserWithRole | undefined;
|
||||
|
||||
|
||||
@@ -74,6 +74,14 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
adminRole: "admin",
|
||||
...options,
|
||||
};
|
||||
const ERROR_CODES = {
|
||||
FAILED_TO_CREATE_USER: "Failed to create user",
|
||||
USER_ALREADY_EXISTS: "User already exists",
|
||||
USER_NOT_FOUND: "User not found",
|
||||
YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself",
|
||||
ONLY_ADMINS_CAN_ACCESS_THIS_ENDPOINT:
|
||||
"Only admins can access this endpoint",
|
||||
} as const;
|
||||
const adminMiddleware = createAuthMiddleware(async (ctx) => {
|
||||
const session = await getSessionFromCtx(ctx);
|
||||
if (!session?.session) {
|
||||
@@ -278,7 +286,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
);
|
||||
if (existUser) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User already exists",
|
||||
message: ERROR_CODES.USER_ALREADY_EXISTS,
|
||||
});
|
||||
}
|
||||
const user =
|
||||
@@ -291,7 +299,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
|
||||
if (!user) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to create user",
|
||||
message: ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
});
|
||||
}
|
||||
const hashedPassword = await ctx.context.password.hash(
|
||||
@@ -589,7 +597,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
async (ctx) => {
|
||||
if (ctx.body.userId === ctx.context.session.user.id) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "You cannot ban yourself",
|
||||
message: ERROR_CODES.YOU_CANNOT_BAN_YOURSELF,
|
||||
});
|
||||
}
|
||||
const user = await ctx.context.internalAdapter.updateUser(
|
||||
@@ -674,7 +682,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
);
|
||||
if (!session) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to create session",
|
||||
message: ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
});
|
||||
}
|
||||
await setSessionCookie(
|
||||
@@ -821,6 +829,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
},
|
||||
),
|
||||
},
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
schema: mergeSchema(schema, opts.schema),
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -6,22 +6,23 @@ import { anonymousClient } from "./client";
|
||||
|
||||
describe("anonymous", async () => {
|
||||
const linkAccountFn = vi.fn();
|
||||
const { customFetchImpl, sessionSetter, testUser } = await getTestInstance({
|
||||
plugins: [
|
||||
anonymous({
|
||||
async onLinkAccount(data) {
|
||||
linkAccountFn(data);
|
||||
},
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
isAnonymous: "is_anon",
|
||||
const { customFetchImpl, auth, sessionSetter, testUser } =
|
||||
await getTestInstance({
|
||||
plugins: [
|
||||
anonymous({
|
||||
async onLinkAccount(data) {
|
||||
linkAccountFn(data);
|
||||
},
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
isAnonymous: "is_anon",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
],
|
||||
});
|
||||
const headers = new Headers();
|
||||
const client = createAuthClient({
|
||||
plugins: [anonymousClient()],
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
User,
|
||||
} from "../../types";
|
||||
import { parseSetCookieHeader, setSessionCookie } from "../../cookies";
|
||||
import { z } from "zod";
|
||||
import { getOrigin } from "../../utils/url";
|
||||
import { mergeSchema } from "../../db/schema";
|
||||
|
||||
@@ -62,6 +61,12 @@ const schema = {
|
||||
} satisfies PluginSchema;
|
||||
|
||||
export const anonymous = (options?: AnonymousOptions) => {
|
||||
const ERROR_CODES = {
|
||||
FAILED_TO_CREATE_USER: "Failed to create user",
|
||||
COULD_NOT_CREATE_SESSION: "Could not create session",
|
||||
ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY:
|
||||
"Anonymous users cannot sign in again anonymously",
|
||||
} as const;
|
||||
return {
|
||||
id: "anonymous",
|
||||
endpoints: {
|
||||
@@ -113,7 +118,7 @@ export const anonymous = (options?: AnonymousOptions) => {
|
||||
return ctx.json(null, {
|
||||
status: 500,
|
||||
body: {
|
||||
message: "Failed to create user",
|
||||
message: ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
status: 500,
|
||||
},
|
||||
});
|
||||
@@ -126,7 +131,7 @@ export const anonymous = (options?: AnonymousOptions) => {
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Could not create session",
|
||||
message: ERROR_CODES.COULD_NOT_CREATE_SESSION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -175,7 +180,8 @@ export const anonymous = (options?: AnonymousOptions) => {
|
||||
}
|
||||
if (ctx.path === "/sign-in/anonymous") {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Anonymous users cannot sign in again anonymously",
|
||||
message:
|
||||
ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,5 +204,6 @@ export const anonymous = (options?: AnonymousOptions) => {
|
||||
],
|
||||
},
|
||||
schema: mergeSchema(schema, options?.schema),
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { INVALID, z } from "zod";
|
||||
import { APIError, createAuthEndpoint } from "../../api";
|
||||
import type { BetterAuthPlugin, User } from "../../types";
|
||||
import { alphabet, generateRandomString } from "../../crypto";
|
||||
@@ -49,6 +49,12 @@ export const emailOTP = (options: EmailOTPOptions) => {
|
||||
otpLength: 6,
|
||||
...options,
|
||||
};
|
||||
const ERROR_CODES = {
|
||||
OTP_EXPIRED: "otp expired",
|
||||
INVALID_OTP: "invalid otp",
|
||||
INVALID_EMAIL: "invalid email",
|
||||
USER_NOT_FOUND: "user not found",
|
||||
} as const;
|
||||
return {
|
||||
id: "email-otp",
|
||||
endpoints: {
|
||||
@@ -100,7 +106,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid email",
|
||||
message: ERROR_CODES.INVALID_EMAIL,
|
||||
});
|
||||
}
|
||||
const otp = generateRandomString(opts.otpLength, alphabet("0-9"));
|
||||
@@ -522,7 +528,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
|
||||
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found",
|
||||
message: ERROR_CODES.USER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const verificationValue =
|
||||
@@ -536,13 +542,13 @@ export const emailOTP = (options: EmailOTPOptions) => {
|
||||
);
|
||||
}
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid OTP",
|
||||
message: ERROR_CODES.OTP_EXPIRED,
|
||||
});
|
||||
}
|
||||
const otp = ctx.body.otp;
|
||||
if (verificationValue.value !== otp) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid OTP",
|
||||
message: ERROR_CODES.INVALID_OTP,
|
||||
});
|
||||
}
|
||||
await ctx.context.internalAdapter.deleteVerificationValue(
|
||||
@@ -597,5 +603,6 @@ export const emailOTP = (options: EmailOTPOptions) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -166,6 +166,9 @@ async function getUserInfo(
|
||||
* A generic OAuth plugin that can be used to add OAuth support to any provider
|
||||
*/
|
||||
export const genericOAuth = (options: GenericOAuthOptions) => {
|
||||
const ERROR_CODES = {
|
||||
INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration",
|
||||
} as const;
|
||||
return {
|
||||
id: "generic-oauth",
|
||||
init: (ctx) => {
|
||||
@@ -352,7 +355,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
|
||||
}
|
||||
if (!finalAuthUrl || !finalTokenUrl) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid OAuth configuration.",
|
||||
message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
|
||||
});
|
||||
}
|
||||
const { state, codeVerifier } = await generateState(ctx);
|
||||
@@ -555,5 +558,6 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
|
||||
},
|
||||
),
|
||||
},
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { APIError } from "better-call";
|
||||
import { setSessionCookie } from "../../cookies";
|
||||
import { alphabet, generateRandomString } from "../../crypto";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
interface MagicLinkOptions {
|
||||
/**
|
||||
@@ -95,7 +96,7 @@ export const magicLink = (options: MagicLinkOptions) => {
|
||||
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found",
|
||||
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -205,10 +206,12 @@ export const magicLink = (options: MagicLinkOptions) => {
|
||||
});
|
||||
userId = newUser.id;
|
||||
if (!userId) {
|
||||
throw ctx.redirect(`${toRedirectTo}?error=USER_NOT_CREATED`);
|
||||
throw ctx.redirect(
|
||||
`${toRedirectTo}?error=failed_to_create_user`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ctx.redirect(`${toRedirectTo}?error=USER_NOT_FOUND`);
|
||||
throw ctx.redirect(`${toRedirectTo}?error=failed_to_create_user`);
|
||||
}
|
||||
}
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
@@ -216,7 +219,9 @@ export const magicLink = (options: MagicLinkOptions) => {
|
||||
ctx.headers,
|
||||
);
|
||||
if (!session) {
|
||||
throw ctx.redirect(`${toRedirectTo}?error=SESSION_NOT_CREATED`);
|
||||
throw ctx.redirect(
|
||||
`${toRedirectTo}?error=failed_to_create_session`,
|
||||
);
|
||||
}
|
||||
await setSessionCookie(ctx, {
|
||||
session,
|
||||
|
||||
@@ -30,6 +30,10 @@ export const multiSession = (options?: MultiSessionConfig) => {
|
||||
|
||||
const isMultiSessionCookie = (key: string) => key.includes("_multi-");
|
||||
|
||||
const ERROR_CODES = {
|
||||
INVALID_SESSION_TOKEN: "Invalid session token",
|
||||
} as const;
|
||||
|
||||
return {
|
||||
id: "multi-session",
|
||||
endpoints: {
|
||||
@@ -109,7 +113,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
|
||||
);
|
||||
if (!sessionCookie) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid session token",
|
||||
message: ERROR_CODES.INVALID_SESSION_TOKEN,
|
||||
});
|
||||
}
|
||||
const session =
|
||||
@@ -120,7 +124,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
|
||||
maxAge: 0,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid session token",
|
||||
message: ERROR_CODES.INVALID_SESSION_TOKEN,
|
||||
});
|
||||
}
|
||||
await setSessionCookie(ctx, session);
|
||||
@@ -170,7 +174,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
|
||||
);
|
||||
if (!sessionCookie) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid session token",
|
||||
message: ERROR_CODES.INVALID_SESSION_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,5 +288,6 @@ export const multiSession = (options?: MultiSessionConfig) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export const ORGANIZATION_ERROR_CODES = {
|
||||
YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION:
|
||||
"You are not allowed to create a new organization",
|
||||
YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS:
|
||||
"You have reached the maximum number of organizations",
|
||||
ORGANIZATION_ALREADY_EXISTS: "Organization already exists",
|
||||
ORGANIZATION_NOT_FOUND: "Organization not found",
|
||||
USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION:
|
||||
"User is not a member of the organization",
|
||||
YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION:
|
||||
"You are not allowed to update this organization",
|
||||
YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION:
|
||||
"You are not allowed to delete this organization",
|
||||
NO_ACTIVE_ORGANIZATION: "No active organization",
|
||||
USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION:
|
||||
"User is already a member of this organization",
|
||||
MEMBER_NOT_FOUND: "Member not found",
|
||||
ROLE_NOT_FOUND: "Role not found",
|
||||
YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER:
|
||||
"You cannot leave the organization as the only owner",
|
||||
YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER:
|
||||
"You are not allowed to delete this member",
|
||||
YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION:
|
||||
"You are not allowed to invite users to this organization",
|
||||
USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION:
|
||||
"User is already invited to this organization",
|
||||
INVITATION_NOT_FOUND: "Invitation not found",
|
||||
YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION:
|
||||
"You are not the recipient of the invitation",
|
||||
YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION:
|
||||
"You are not allowed to cancel this invitation",
|
||||
INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION:
|
||||
"Inviter is no longer a member of the organization",
|
||||
} as const;
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from "./routes/crud-org";
|
||||
import type { Invitation, Member, Organization } from "./schema";
|
||||
import type { Prettify } from "../../types/helper";
|
||||
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
|
||||
|
||||
export interface OrganizationOptions {
|
||||
/**
|
||||
@@ -308,7 +309,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
async (ctx) => {
|
||||
if (!ctx.context.session.session.activeOrganizationId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "No active organization",
|
||||
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const adapter = getOrgAdapter(ctx.context);
|
||||
@@ -319,7 +320,8 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
});
|
||||
if (!member) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "You are not a member of this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const role = roles[member.role as keyof typeof roles];
|
||||
@@ -481,5 +483,6 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
}
|
||||
>,
|
||||
},
|
||||
$ERROR_CODES: ORGANIZATION_ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { orgMiddleware, orgSessionMiddleware } from "../call";
|
||||
import { type InferRolesFromOption } from "../schema";
|
||||
import { APIError } from "better-call";
|
||||
import type { OrganizationOptions } from "../organization";
|
||||
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
|
||||
|
||||
export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
option: O,
|
||||
@@ -99,7 +100,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
ctx.body.organizationId || session.session.activeOrganizationId;
|
||||
if (!organizationId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Organization not found",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
||||
@@ -109,13 +110,13 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
});
|
||||
if (!member) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const role = ctx.context.roles[member.role];
|
||||
if (!role) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Role not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const canInvite = role.authorize({
|
||||
@@ -123,7 +124,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
});
|
||||
if (canInvite.error) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not allowed to invite members",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const alreadyMember = await adapter.findMemberByEmail({
|
||||
@@ -132,7 +134,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
});
|
||||
if (alreadyMember) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User is already a member of this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const alreadyInvited = await adapter.findPendingInvitation({
|
||||
@@ -141,7 +144,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
});
|
||||
if (alreadyInvited.length && !ctx.body.resend) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User is already invited to this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const invitation = await adapter.createInvitation({
|
||||
@@ -157,7 +161,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
|
||||
if (!organization) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Organization not found",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,12 +228,13 @@ export const acceptInvitation = createAuthEndpoint(
|
||||
invitation.status !== "pending"
|
||||
) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invitation not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
if (invitation.email !== session.user.email) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not the recipient of the invitation",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
|
||||
});
|
||||
}
|
||||
const acceptedI = await adapter.updateInvitation({
|
||||
@@ -250,7 +255,7 @@ export const acceptInvitation = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Invitation not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -311,7 +316,8 @@ export const rejectInvitation = createAuthEndpoint(
|
||||
}
|
||||
if (invitation.email !== session.user.email) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not the recipient of the invitation",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
|
||||
});
|
||||
}
|
||||
const rejectedI = await adapter.updateInvitation({
|
||||
@@ -362,7 +368,7 @@ export const cancelInvitation = createAuthEndpoint(
|
||||
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
||||
if (!invitation) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invitation not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const member = await adapter.findMemberByOrgId({
|
||||
@@ -371,7 +377,7 @@ export const cancelInvitation = createAuthEndpoint(
|
||||
});
|
||||
if (!member) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const canCancel = ctx.context.roles[member.role].authorize({
|
||||
@@ -379,7 +385,8 @@ export const cancelInvitation = createAuthEndpoint(
|
||||
});
|
||||
if (canCancel.error) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not allowed to cancel this invitation",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
|
||||
});
|
||||
}
|
||||
const canceledI = await adapter.updateInvitation({
|
||||
@@ -483,7 +490,8 @@ export const getInvitation = createAuthEndpoint(
|
||||
}
|
||||
if (invitation.email !== session.user.email) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not the recipient of the invitation",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
|
||||
});
|
||||
}
|
||||
const organization = await adapter.findOrganizationById(
|
||||
@@ -491,7 +499,7 @@ export const getInvitation = createAuthEndpoint(
|
||||
);
|
||||
if (!organization) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Organization not found",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const member = await adapter.findMemberByOrgId({
|
||||
@@ -500,7 +508,8 @@ export const getInvitation = createAuthEndpoint(
|
||||
});
|
||||
if (!member) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Inviter is no longer a member of the organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { User } from "../../../db/schema";
|
||||
import { generateId } from "../../../utils";
|
||||
import type { OrganizationOptions } from "../organization";
|
||||
import { getSessionFromCtx } from "../../../api";
|
||||
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
|
||||
import { BASE_ERROR_CODES } from "../../../error/codes";
|
||||
|
||||
export const addMember = <O extends OrganizationOptions>() =>
|
||||
createAuthEndpoint(
|
||||
@@ -38,7 +40,7 @@ export const addMember = <O extends OrganizationOptions>() =>
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "No active organization found!",
|
||||
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -51,7 +53,7 @@ export const addMember = <O extends OrganizationOptions>() =>
|
||||
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found!",
|
||||
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +63,8 @@ export const addMember = <O extends OrganizationOptions>() =>
|
||||
});
|
||||
if (alreadyMember) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User is already a member of this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +146,7 @@ export const removeMember = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "No active organization found!",
|
||||
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -154,13 +157,13 @@ export const removeMember = createAuthEndpoint(
|
||||
});
|
||||
if (!member) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const role = ctx.context.roles[member.role];
|
||||
if (!role) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Role not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const isLeaving =
|
||||
@@ -171,7 +174,8 @@ export const removeMember = createAuthEndpoint(
|
||||
member.role === (ctx.context.orgOptions?.creatorRole || "owner");
|
||||
if (isOwnerLeaving) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "You cannot leave the organization as the owner",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,7 +186,8 @@ export const removeMember = createAuthEndpoint(
|
||||
}).success;
|
||||
if (!canDeleteMember) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "You are not allowed to delete this member",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER,
|
||||
});
|
||||
}
|
||||
let existing: Member | null = null;
|
||||
@@ -196,7 +201,7 @@ export const removeMember = createAuthEndpoint(
|
||||
}
|
||||
if (existing?.organizationId !== organizationId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
await adapter.deleteMember(existing.id);
|
||||
@@ -273,7 +278,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "No active organization found!",
|
||||
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -286,7 +291,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -295,7 +300,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Role not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -325,7 +330,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -378,7 +383,7 @@ export const getActiveMember = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "No active organization found!",
|
||||
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -391,7 +396,7 @@ export const getActiveMember = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Member not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getOrgAdapter } from "../adapter";
|
||||
import { orgMiddleware, orgSessionMiddleware } from "../call";
|
||||
import { APIError } from "better-call";
|
||||
import { setSessionCookie } from "../../../cookies";
|
||||
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
|
||||
import { getSessionFromCtx } from "../../../api";
|
||||
|
||||
export const createOrganization = createAuthEndpoint(
|
||||
@@ -83,7 +84,8 @@ export const createOrganization = createAuthEndpoint(
|
||||
|
||||
if (!canCreateOrg) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not allowed to create an organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const adapter = getOrgAdapter(ctx.context, options);
|
||||
@@ -98,7 +100,8 @@ export const createOrganization = createAuthEndpoint(
|
||||
|
||||
if (hasReachedOrgLimit) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You have reached the organization limit",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ export const createOrganization = createAuthEndpoint(
|
||||
);
|
||||
if (existingOrganization) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Organization with this slug already exists",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS,
|
||||
});
|
||||
}
|
||||
const organization = await adapter.createOrganization({
|
||||
@@ -197,7 +200,7 @@ export const updateOrganization = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Organization id not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -210,7 +213,8 @@ export const updateOrganization = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "User is not a member of this organization!",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -229,7 +233,8 @@ export const updateOrganization = createAuthEndpoint(
|
||||
if (canUpdateOrg.error) {
|
||||
return ctx.json(null, {
|
||||
body: {
|
||||
message: "You are not allowed to update this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
|
||||
},
|
||||
status: 403,
|
||||
});
|
||||
@@ -284,7 +289,7 @@ export const deleteOrganization = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Organization id not found!",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -297,7 +302,8 @@ export const deleteOrganization = createAuthEndpoint(
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "User is not a member of this organization!",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -315,7 +321,8 @@ export const deleteOrganization = createAuthEndpoint(
|
||||
});
|
||||
if (canDeleteOrg.error) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not allowed to delete this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
if (organizationId === session.session.activeOrganizationId) {
|
||||
@@ -387,7 +394,7 @@ export const getFullOrganization = createAuthEndpoint(
|
||||
});
|
||||
if (!organization) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Organization not found",
|
||||
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
return ctx.json(organization);
|
||||
@@ -470,7 +477,8 @@ export const setActiveOrganization = createAuthEndpoint(
|
||||
if (!isMember) {
|
||||
await adapter.setActiveOrganization(session.session.token, null);
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "You are not a member of this organization",
|
||||
message:
|
||||
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
const updatedSession = await adapter.setActiveOrganization(
|
||||
|
||||
@@ -107,6 +107,17 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
const maxAgeInSeconds = Math.floor(
|
||||
(expirationTime.getTime() - currentTime.getTime()) / 1000,
|
||||
);
|
||||
|
||||
const ERROR_CODES = {
|
||||
CHALLENGE_NOT_FOUND: "Challenge not found",
|
||||
YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:
|
||||
"You are not allowed to register this passkey",
|
||||
FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
|
||||
PASSKEY_NOT_FOUND: "Passkey not found",
|
||||
AUTHENTICATION_FAILED: "Authentication failed",
|
||||
UNABLE_TO_CREATE_SESSION: "Unable to create session",
|
||||
FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey",
|
||||
} as const;
|
||||
return {
|
||||
id: "passkey",
|
||||
endpoints: {
|
||||
@@ -493,7 +504,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
);
|
||||
if (!challengeId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Challenge not found",
|
||||
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -512,7 +523,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
|
||||
if (userData.id !== ctx.context.session.user.id) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "You are not authorized to register this passkey",
|
||||
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -559,7 +570,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to verify registration",
|
||||
message: ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -611,7 +622,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
);
|
||||
if (!challengeId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Challenge not found",
|
||||
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -621,7 +632,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
);
|
||||
if (!data) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Challenge not found",
|
||||
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const { expectedChallenge } = JSON.parse(
|
||||
@@ -638,7 +649,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
});
|
||||
if (!passkey) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Passkey not found",
|
||||
message: ERROR_CODES.PASSKEY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
try {
|
||||
@@ -662,7 +673,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
const { verified } = verification;
|
||||
if (!verified)
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Authentication failed",
|
||||
message: ERROR_CODES.AUTHENTICATION_FAILED,
|
||||
});
|
||||
|
||||
await ctx.context.adapter.update<Passkey>({
|
||||
@@ -683,7 +694,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
);
|
||||
if (!s) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Unable to create session",
|
||||
message: ERROR_CODES.UNABLE_TO_CREATE_SESSION,
|
||||
});
|
||||
}
|
||||
const user = await ctx.context.internalAdapter.findUserById(
|
||||
@@ -709,7 +720,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
} catch (e) {
|
||||
ctx.context.logger.error("Failed to verify authentication", e);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Failed to verify authentication",
|
||||
message: ERROR_CODES.AUTHENTICATION_FAILED,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -777,13 +788,13 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
|
||||
if (!passkey) {
|
||||
throw new APIError("NOT_FOUND", {
|
||||
message: "Passkey not found",
|
||||
message: ERROR_CODES.PASSKEY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
if (passkey.userId !== ctx.context.session.user.id) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "You are not authorized to update this passkey",
|
||||
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -802,7 +813,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
|
||||
if (!updatedPasskey) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to update passkey",
|
||||
message: ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
|
||||
});
|
||||
}
|
||||
return ctx.json(
|
||||
@@ -817,6 +828,7 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
),
|
||||
},
|
||||
schema: mergeSchema(schema, options?.schema),
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { alphabet, generateRandomString } from "../../crypto/random";
|
||||
import { getSessionFromCtx } from "../../api";
|
||||
import { getDate } from "../../utils/date";
|
||||
import { setSessionCookie } from "../../cookies";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export interface UserWithPhoneNumber extends User {
|
||||
phoneNumber: string;
|
||||
@@ -101,6 +102,13 @@ export const phoneNumber = (options?: {
|
||||
code: "code",
|
||||
createdAt: "createdAt",
|
||||
};
|
||||
|
||||
const ERROR_CODES = {
|
||||
INVALID_PHONE_NUMBER: "Invalid phone number",
|
||||
INVALID_PHONE_NUMBER_OR_PASSWORD: "Invalid phone number or password",
|
||||
UNEXPECTED_ERROR: "Unexpected error",
|
||||
OTP_NOT_FOUND: "OTP not found",
|
||||
} as const;
|
||||
return {
|
||||
id: "phone-number",
|
||||
endpoints: {
|
||||
@@ -160,7 +168,7 @@ export const phoneNumber = (options?: {
|
||||
);
|
||||
if (!isValidNumber) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid phone number!",
|
||||
message: ERROR_CODES.INVALID_PHONE_NUMBER,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -176,7 +184,7 @@ export const phoneNumber = (options?: {
|
||||
});
|
||||
if (!user) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid phone number or password",
|
||||
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const accounts =
|
||||
@@ -189,14 +197,14 @@ export const phoneNumber = (options?: {
|
||||
phoneNumber,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid password or password",
|
||||
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const currentPassword = credentialAccount?.password;
|
||||
if (!currentPassword) {
|
||||
ctx.context.logger.error("Password not found", { phoneNumber });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Unexpected error",
|
||||
message: ERROR_CODES.UNEXPECTED_ERROR,
|
||||
});
|
||||
}
|
||||
const validPassword = await ctx.context.password.verify({
|
||||
@@ -206,7 +214,7 @@ export const phoneNumber = (options?: {
|
||||
if (!validPassword) {
|
||||
ctx.context.logger.error("Invalid password");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid email or password",
|
||||
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
@@ -217,7 +225,7 @@ export const phoneNumber = (options?: {
|
||||
if (!session) {
|
||||
ctx.context.logger.error("Failed to create session");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Failed to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,7 +290,7 @@ export const phoneNumber = (options?: {
|
||||
);
|
||||
if (!isValidNumber) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid phone number!",
|
||||
message: ERROR_CODES.INVALID_PHONE_NUMBER,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -391,7 +399,7 @@ export const phoneNumber = (options?: {
|
||||
});
|
||||
}
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "OTP not found",
|
||||
message: ERROR_CODES.OTP_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
if (otp.value !== ctx.body.code) {
|
||||
@@ -406,7 +414,7 @@ export const phoneNumber = (options?: {
|
||||
const session = await getSessionFromCtx(ctx);
|
||||
if (!session) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Session not found",
|
||||
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const user = await ctx.context.internalAdapter.updateUser(
|
||||
@@ -454,7 +462,7 @@ export const phoneNumber = (options?: {
|
||||
});
|
||||
if (!user) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to create user",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -468,7 +476,7 @@ export const phoneNumber = (options?: {
|
||||
|
||||
if (!user) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to update user",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_UPDATE_USER,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,7 +487,7 @@ export const phoneNumber = (options?: {
|
||||
);
|
||||
if (!session) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Failed to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
|
||||
});
|
||||
}
|
||||
await setSessionCookie(ctx, {
|
||||
@@ -500,6 +508,7 @@ export const phoneNumber = (options?: {
|
||||
),
|
||||
},
|
||||
schema: mergeSchema(schema, options?.schema),
|
||||
$ERROR_CODES: ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../types";
|
||||
import { APIError } from "better-call";
|
||||
import { setSessionCookie } from "../../../cookies";
|
||||
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
|
||||
|
||||
export interface BackupCodeOptions {
|
||||
/**
|
||||
@@ -121,7 +122,7 @@ export const backupCode2fa = (
|
||||
});
|
||||
if (!twoFactor) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Backup codes aren't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
const validate = await verifyBackupCode(
|
||||
@@ -133,7 +134,7 @@ export const backupCode2fa = (
|
||||
);
|
||||
if (!validate.status) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid backup code",
|
||||
message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
|
||||
});
|
||||
}
|
||||
const updatedBackupCodes = await symmetricEncrypt({
|
||||
@@ -179,7 +180,7 @@ export const backupCode2fa = (
|
||||
const user = ctx.context.session.user as UserWithTwoFactor;
|
||||
if (!user.twoFactorEnabled) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Two factor isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
await ctx.context.password.checkPassword(user.id, ctx);
|
||||
@@ -237,7 +238,7 @@ export const backupCode2fa = (
|
||||
);
|
||||
if (!backupCodes) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Backup codes aren't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
return ctx.json({
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const TWO_FACTOR_ERROR_CODES = {
|
||||
OTP_NOT_ENABLED: "OTP not enabled",
|
||||
OTP_HAS_EXPIRED: "OTP has expired",
|
||||
TOTP_NOT_ENABLED: "TOTP not enabled",
|
||||
TWO_FACTOR_NOT_ENABLED: "Two factor isn't enabled",
|
||||
BACKUP_CODES_NOT_ENABLED: "Backup codes aren't enabled",
|
||||
INVALID_BACKUP_CODE: "Invalid backup code",
|
||||
} as const;
|
||||
@@ -17,6 +17,7 @@ import { TimeSpan } from "oslo";
|
||||
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
|
||||
import { getEndpointResponse } from "../../utils/plugin-helper";
|
||||
import { schema } from "./schema";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
export const twoFactor = (options?: TwoFactorOptions) => {
|
||||
const opts = {
|
||||
@@ -41,6 +42,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
||||
},
|
||||
opts.twoFactorTable,
|
||||
);
|
||||
|
||||
return {
|
||||
id: "two-factor",
|
||||
endpoints: {
|
||||
@@ -101,7 +103,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
||||
});
|
||||
if (!isPasswordValid) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid password",
|
||||
message: BASE_ERROR_CODES.INVALID_PASSWORD,
|
||||
});
|
||||
}
|
||||
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "../types";
|
||||
import { TimeSpan } from "oslo";
|
||||
import { alphabet, generateRandomString } from "../../../crypto";
|
||||
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
|
||||
|
||||
export interface OTPOptions {
|
||||
/**
|
||||
@@ -111,7 +112,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
|
||||
});
|
||||
if (!twoFactor) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "OTP isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.OTP_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
const code = generateRandomString(opts.digits, alphabet("0-9"));
|
||||
@@ -177,7 +178,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
|
||||
});
|
||||
if (!twoFactor) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "OTP isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.OTP_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
const toCheckOtp =
|
||||
@@ -186,7 +187,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
|
||||
);
|
||||
if (!toCheckOtp || toCheckOtp.expiresAt < new Date()) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "OTP has expired",
|
||||
message: TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED,
|
||||
});
|
||||
}
|
||||
if (toCheckOtp.value === ctx.body.code) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
UserWithTwoFactor,
|
||||
} from "../types";
|
||||
import { setSessionCookie } from "../../../cookies";
|
||||
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
|
||||
|
||||
export type TOTPOptions = {
|
||||
/**
|
||||
@@ -93,7 +94,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
|
||||
});
|
||||
if (!twoFactor) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "totp isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
const totp = new TOTPController(opts);
|
||||
@@ -157,7 +158,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
|
||||
});
|
||||
if (!twoFactor || !user.twoFactorEnabled) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "totp isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
await ctx.context.password.checkPassword(user.id, ctx);
|
||||
@@ -228,7 +229,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
|
||||
|
||||
if (!twoFactor) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "totp isn't enabled",
|
||||
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
|
||||
});
|
||||
}
|
||||
const totp = new TOTPController(opts);
|
||||
|
||||
@@ -5,8 +5,15 @@ import { APIError } from "better-call";
|
||||
import type { Account, User } from "../../db/schema";
|
||||
import { setSessionCookie } from "../../cookies";
|
||||
import { sendVerificationEmailFn } from "../../api";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
import { TWO_FACTOR_ERROR_CODES } from "../two-factor/error-code";
|
||||
|
||||
export const username = () => {
|
||||
const ERROR_CODES = {
|
||||
INVALID_USERNAME_OR_PASSWORD: "invalid username or password",
|
||||
EMAIL_NOT_VERIFIED: "email not verified",
|
||||
UNEXPECTED_ERROR: "unexpected error",
|
||||
};
|
||||
return {
|
||||
id: "username",
|
||||
endpoints: {
|
||||
@@ -68,7 +75,7 @@ export const username = () => {
|
||||
await ctx.context.password.hash(ctx.body.password);
|
||||
ctx.context.logger.error("User not found", { username });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid username or password",
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +85,7 @@ export const username = () => {
|
||||
) {
|
||||
await sendVerificationEmailFn(ctx, user);
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Email not verified",
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,14 +104,14 @@ export const username = () => {
|
||||
});
|
||||
if (!account) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid username or password",
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const currentPassword = account?.password;
|
||||
if (!currentPassword) {
|
||||
ctx.context.logger.error("Password not found", { username });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Unexpected error",
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const validPassword = await ctx.context.password.verify({
|
||||
@@ -114,7 +121,7 @@ export const username = () => {
|
||||
if (!validPassword) {
|
||||
ctx.context.logger.error("Invalid password");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid username or password",
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
@@ -126,7 +133,7 @@ export const username = () => {
|
||||
return ctx.json(null, {
|
||||
status: 500,
|
||||
body: {
|
||||
message: "Failed to create session",
|
||||
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
|
||||
status: 500,
|
||||
},
|
||||
});
|
||||
@@ -155,5 +162,6 @@ export const username = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
$ERROR_CODES: TWO_FACTOR_ERROR_CODES,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Migration } from "kysely";
|
||||
import type { AuthEndpoint } from "../api/call";
|
||||
import type { FieldAttribute } from "../db/field";
|
||||
import type { HookEndpointContext } from "./context";
|
||||
import type { DeepPartial, LiteralString } from "./helper";
|
||||
import type { DeepPartial, LiteralString, UnionToIntersection } from "./helper";
|
||||
import type { AuthContext, BetterAuthOptions } from ".";
|
||||
|
||||
export type PluginSchema = {
|
||||
@@ -137,6 +137,10 @@ export type BetterAuthPlugin = {
|
||||
max: number;
|
||||
pathMatcher: (path: string) => boolean;
|
||||
}[];
|
||||
/**
|
||||
* The error codes returned by the plugin
|
||||
*/
|
||||
$ERROR_CODES?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type InferOptionSchema<S extends PluginSchema> = S extends Record<
|
||||
@@ -152,3 +156,14 @@ export type InferOptionSchema<S extends PluginSchema> = S extends Record<
|
||||
};
|
||||
}
|
||||
: never;
|
||||
|
||||
export type InferPluginErrorCodes<O extends BetterAuthOptions> =
|
||||
O["plugins"] extends Array<infer P>
|
||||
? UnionToIntersection<
|
||||
P extends BetterAuthPlugin
|
||||
? P["$ERROR_CODES"] extends Record<string, any>
|
||||
? P["$ERROR_CODES"]
|
||||
: {}
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
|
||||
Reference in New Issue
Block a user