chore: migrate signup API to appDir (#19975)
* refactor calcomHandler * refactor selfHostedHandler * fix * migrate to appdir * fix type checks
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import type { NextApiResponse } from "next";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHandler";
|
||||
import selfHostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler";
|
||||
import { type RequestWithUsernameStatus } from "@calcom/features/auth/signup/username";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
@@ -11,12 +12,12 @@ import logger from "@calcom/lib/logger";
|
||||
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
async function ensureSignupIsEnabled(req: RequestWithUsernameStatus) {
|
||||
async function ensureSignupIsEnabled(body: Record<string, string>) {
|
||||
const { token } = signupSchema
|
||||
.pick({
|
||||
token: true,
|
||||
})
|
||||
.parse(req.body);
|
||||
.parse(body);
|
||||
|
||||
// Still allow signups if there is a team invite
|
||||
if (token) return;
|
||||
@@ -32,26 +33,17 @@ async function ensureSignupIsEnabled(req: RequestWithUsernameStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureReqIsPost(req: RequestWithUsernameStatus) {
|
||||
if (req.method !== "POST") {
|
||||
throw new HttpError({
|
||||
statusCode: 405,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
async function handler(req: NextRequest) {
|
||||
const remoteIp = getIP(req);
|
||||
// Use a try catch instead of returning res every time
|
||||
try {
|
||||
const body = await parseRequestData(req);
|
||||
await checkCfTurnstileToken({
|
||||
token: req.headers["cf-access-token"] as string,
|
||||
token: req.headers.get("cf-access-token") as string,
|
||||
remoteIp,
|
||||
});
|
||||
|
||||
ensureReqIsPost(req);
|
||||
await ensureSignupIsEnabled(req);
|
||||
await ensureSignupIsEnabled(body);
|
||||
|
||||
/**
|
||||
* Im not sure its worth merging these two handlers. They are different enough to be separate.
|
||||
@@ -61,15 +53,17 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA
|
||||
* @zomars: We need to be able to test this with E2E. They way it's done RN it will never run on CI.
|
||||
*/
|
||||
if (IS_PREMIUM_USERNAME_ENABLED) {
|
||||
return await calcomSignupHandler(req, res);
|
||||
return await calcomSignupHandler(body);
|
||||
}
|
||||
|
||||
return await selfHostedSignupHandler(req, res);
|
||||
return await selfHostedSignupHandler(body);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) {
|
||||
return res.status(e.statusCode).json({ message: e.message });
|
||||
return NextResponse.json({ message: e.message }, { status: e.statusCode });
|
||||
}
|
||||
logger.error(e);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NextApiResponse } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
@@ -11,12 +12,14 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { usernameHandler, type RequestWithUsernameStatus } from "@calcom/lib/server/username";
|
||||
import type { CustomNextApiHandler } from "@calcom/lib/server/username";
|
||||
import { usernameHandler } from "@calcom/lib/server/username";
|
||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx";
|
||||
|
||||
import { joinAnyChildTeamOnOrgInvite } from "../utils/organization";
|
||||
import {
|
||||
@@ -27,7 +30,7 @@ import {
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
|
||||
|
||||
async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
const handler: CustomNextApiHandler = async (body, usernameStatus) => {
|
||||
const {
|
||||
email: _email,
|
||||
password,
|
||||
@@ -38,7 +41,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
password: true,
|
||||
token: true,
|
||||
})
|
||||
.parse(req.body);
|
||||
.parse(body);
|
||||
|
||||
const billingService = new StripeBillingService();
|
||||
|
||||
@@ -46,12 +49,12 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
|
||||
log.debug("handler", { email: _email });
|
||||
|
||||
let username: string | null = req.usernameStatus.requestedUserName;
|
||||
let username: string | null = usernameStatus.requestedUserName;
|
||||
let checkoutSessionId: string | null = null;
|
||||
|
||||
// Check for premium username
|
||||
if (req.usernameStatus.statusCode === 418) {
|
||||
return res.status(req.usernameStatus.statusCode).json(req.usernameStatus.json);
|
||||
if (usernameStatus.statusCode === 418) {
|
||||
return NextResponse.json(usernameStatus.json, { status: 418 });
|
||||
}
|
||||
|
||||
// Validate the user
|
||||
@@ -110,7 +113,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
const returnUrl = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=/auth/verify?sessionId={CHECKOUT_SESSION_ID}`;
|
||||
|
||||
// Pro username, must be purchased
|
||||
if (req.usernameStatus.statusCode === 402) {
|
||||
if (usernameStatus.statusCode === 402) {
|
||||
const checkoutSession = await billingService.createSubscriptionCheckout({
|
||||
mode: "subscription",
|
||||
customerId: customer.stripeCustomerId,
|
||||
@@ -207,20 +210,23 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
}
|
||||
sendEmailVerification({
|
||||
email,
|
||||
language: await getLocaleFromRequest(req),
|
||||
language: await getLocaleFromRequest(buildLegacyRequest(await headers(), await cookies())),
|
||||
username: username || "",
|
||||
});
|
||||
}
|
||||
|
||||
if (checkoutSessionId) {
|
||||
console.log("Created user but missing payment", checkoutSessionId);
|
||||
return res.status(402).json({
|
||||
message: "Created user but missing payment",
|
||||
checkoutSessionId,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ message: "Created user but missing payment", checkoutSessionId },
|
||||
{ status: 402 }
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(201).json({ message: "Created user", stripeCustomerId: customer.stripeCustomerId });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: "Created user", stripeCustomerId: customer.stripeCustomerId },
|
||||
{ status: 201 }
|
||||
);
|
||||
};
|
||||
|
||||
export default usernameHandler(handler);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
@@ -21,16 +21,14 @@ import {
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "../utils/token";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const data = req.body;
|
||||
const { email, password, language, token } = signupSchema.parse(data);
|
||||
export default async function handler(body: Record<string, string>) {
|
||||
const { email, password, language, token } = signupSchema.parse(body);
|
||||
|
||||
const username = slugify(data.username);
|
||||
const username = slugify(body.username);
|
||||
const userEmail = email.toLowerCase();
|
||||
|
||||
if (!username) {
|
||||
res.status(422).json({ message: "Invalid username" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Invalid username" }, { status: 422 });
|
||||
}
|
||||
|
||||
let foundToken: { id: number; teamId: number | null; expires: Date } | null = null;
|
||||
@@ -52,10 +50,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
if (!userValidation.isValid) {
|
||||
logger.error("User validation failed", { userValidation });
|
||||
return res.status(409).json({ message: "Username or email is already taken" });
|
||||
return NextResponse.json({ message: "Username or email is already taken" }, { status: 409 });
|
||||
}
|
||||
if (!userValidation.username) {
|
||||
return res.status(422).json({ message: "Invalid username" });
|
||||
return NextResponse.json({ message: "Invalid username" }, { status: 422 });
|
||||
}
|
||||
correctedUsername = userValidation.username;
|
||||
}
|
||||
@@ -86,8 +84,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (isCheckingUsernameInGlobalNamespace) {
|
||||
const isUsernameAvailable = !(await isUsernameReservedDueToMigration(correctedUsername));
|
||||
if (!isUsernameAvailable) {
|
||||
res.status(409).json({ message: "A user exists with that username" });
|
||||
return;
|
||||
return NextResponse.json({ message: "A user exists with that username" }, { status: 409 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,16 +132,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
} else {
|
||||
const isUsernameAvailable = !(await isUsernameReservedDueToMigration(correctedUsername));
|
||||
if (!isUsernameAvailable) {
|
||||
res.status(409).json({ message: "A user exists with that username" });
|
||||
return;
|
||||
return NextResponse.json({ message: "A user exists with that username" }, { status: 409 });
|
||||
}
|
||||
if (IS_PREMIUM_USERNAME_ENABLED) {
|
||||
const checkUsername = await checkPremiumUsername(correctedUsername);
|
||||
if (checkUsername.premium) {
|
||||
res.status(422).json({
|
||||
message: "Sign up from https://cal.com/signup to claim your premium username",
|
||||
});
|
||||
return;
|
||||
return NextResponse.json(
|
||||
{ message: "Sign up from https://cal.com/signup to claim your premium username" },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
}
|
||||
await prisma.user.upsert({
|
||||
@@ -179,5 +175,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({ message: "Created user" });
|
||||
return NextResponse.json({ message: "Created user" }, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { NextResponse } from "next/server";
|
||||
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -11,30 +11,28 @@ import notEmpty from "../notEmpty";
|
||||
const log = logger.getSubLogger({ prefix: ["server/username"] });
|
||||
const cachedData: Set<string> = new Set();
|
||||
|
||||
export type RequestWithUsernameStatus = NextApiRequest & {
|
||||
usernameStatus: {
|
||||
/**
|
||||
* ```text
|
||||
* 200: Username is available
|
||||
* 402: Pro username, must be purchased
|
||||
* 418: A user exists with that username
|
||||
* ```
|
||||
*/
|
||||
statusCode: 200 | 402 | 418;
|
||||
requestedUserName: string;
|
||||
json: {
|
||||
available: boolean;
|
||||
premium: boolean;
|
||||
message?: string;
|
||||
suggestion?: string;
|
||||
};
|
||||
type UsernameStatus = {
|
||||
/**
|
||||
* ```text
|
||||
* 200: Username is available
|
||||
* 402: Pro username, must be purchased
|
||||
* 418: A user exists with that username
|
||||
* ```
|
||||
*/
|
||||
statusCode: 200 | 402 | 418;
|
||||
requestedUserName: string;
|
||||
json: {
|
||||
available: boolean;
|
||||
premium: boolean;
|
||||
message?: string;
|
||||
suggestion?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CustomNextApiHandler<T = unknown> = (
|
||||
req: RequestWithUsernameStatus,
|
||||
res: NextApiResponse<T>
|
||||
) => void | Promise<void>;
|
||||
export type CustomNextApiHandler = (
|
||||
body: Record<string, string>,
|
||||
usernameStatus: UsernameStatus
|
||||
) => Promise<NextResponse<any>>;
|
||||
|
||||
export async function isBlacklisted(username: string) {
|
||||
// NodeJS forEach is very, very fast (these days) so even though we only have to construct the Set
|
||||
@@ -66,7 +64,7 @@ export const generateUsernameSuggestion = async (users: string[], username: stri
|
||||
const processResult = (
|
||||
result: "ok" | "username_exists" | "is_premium"
|
||||
): // explicitly assign return value to ensure statusCode is typehinted
|
||||
{ statusCode: RequestWithUsernameStatus["usernameStatus"]["statusCode"]; message: string } => {
|
||||
{ statusCode: UsernameStatus["statusCode"]; message: string } => {
|
||||
// using a switch statement instead of multiple ifs to make sure typescript knows
|
||||
// there is only limited options
|
||||
switch (result) {
|
||||
@@ -85,29 +83,27 @@ const processResult = (
|
||||
}
|
||||
};
|
||||
|
||||
const usernameHandler =
|
||||
(handler: CustomNextApiHandler) =>
|
||||
async (req: RequestWithUsernameStatus, res: NextApiResponse): Promise<void> => {
|
||||
const username = slugify(req.body.username);
|
||||
const check = await usernameCheckForSignup({ username, email: req.body.email });
|
||||
const usernameHandler = (handler: CustomNextApiHandler) => async (body: Record<string, string>) => {
|
||||
const username = slugify(body.username);
|
||||
const check = await usernameCheckForSignup({ username, email: body.email });
|
||||
|
||||
let result: Parameters<typeof processResult>[0] = "ok";
|
||||
if (check.premium) result = "is_premium";
|
||||
if (!check.available) result = "username_exists";
|
||||
let result: Parameters<typeof processResult>[0] = "ok";
|
||||
if (check.premium) result = "is_premium";
|
||||
if (!check.available) result = "username_exists";
|
||||
|
||||
const { statusCode, message } = processResult(result);
|
||||
req.usernameStatus = {
|
||||
statusCode,
|
||||
requestedUserName: username,
|
||||
json: {
|
||||
available: result !== "username_exists",
|
||||
premium: result === "is_premium",
|
||||
message,
|
||||
suggestion: check.suggestedUsername,
|
||||
},
|
||||
};
|
||||
return handler(req, res);
|
||||
const { statusCode, message } = processResult(result);
|
||||
const usernameStatus = {
|
||||
statusCode,
|
||||
requestedUserName: username,
|
||||
json: {
|
||||
available: result !== "username_exists",
|
||||
premium: result === "is_premium",
|
||||
message,
|
||||
suggestion: check.suggestedUsername,
|
||||
},
|
||||
};
|
||||
return handler(body, usernameStatus);
|
||||
};
|
||||
|
||||
const usernameCheck = async (usernameRaw: string, currentOrgDomain?: string | null) => {
|
||||
log.debug("usernameCheck", { usernameRaw, currentOrgDomain });
|
||||
|
||||
Reference in New Issue
Block a user