- Wire up real user data to sidebar, header, and account modal - Add functional profile editing (first name, last name) via WorkOS API - Add password change functionality via WorkOS API - Add logout functionality to sidebar and header dropdowns - Migrate from manual WorkOS SDK to @workos-inc/authkit-nextjs - Add server-side input validation with Zod schemas for all auth routes - Add shared validation schemas for auth, users, teams, schedule, financial - Fix 30-second auto-logout by properly handling refresh tokens - Add SidebarUser type and toSidebarUser helper for UI components - Add getInitials utility for avatar fallbacks - Document rate limiting configuration for Cloudflare WAF - Fix login page Suspense boundary for Next.js 15 compatibility - Remove obsolete workos-client.ts in favor of authkit helpers Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
160 lines
4.4 KiB
TypeScript
Executable File
160 lines
4.4 KiB
TypeScript
Executable File
import { NextRequest, NextResponse } from "next/server"
|
|
import { getWorkOS, saveSession } from "@workos-inc/authkit-nextjs"
|
|
import { z } from "zod"
|
|
import { ensureUserExists } from "@/lib/auth"
|
|
|
|
// input validation schema
|
|
const loginRequestSchema = z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("password"),
|
|
email: z.string().email("Please enter a valid email address"),
|
|
password: z.string().min(1, "Password is required"),
|
|
}),
|
|
z.object({
|
|
type: z.literal("passwordless_send"),
|
|
email: z.string().email("Please enter a valid email address"),
|
|
}),
|
|
z.object({
|
|
type: z.literal("passwordless_verify"),
|
|
email: z.string().email("Please enter a valid email address"),
|
|
code: z.string().min(1, "Verification code is required"),
|
|
}),
|
|
])
|
|
|
|
function mapWorkOSError(error: unknown): string {
|
|
const err = error as { code?: string; message?: string }
|
|
switch (err.code) {
|
|
case "invalid_credentials":
|
|
return "Invalid email or password"
|
|
case "user_not_found":
|
|
return "No account found with this email"
|
|
case "expired_code":
|
|
return "Code expired. Please request a new one."
|
|
case "invalid_code":
|
|
return "Invalid code. Please try again."
|
|
default:
|
|
return err.message || "An error occurred. Please try again."
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// validate input
|
|
const body = await request.json()
|
|
const parseResult = loginRequestSchema.safeParse(body)
|
|
|
|
if (!parseResult.success) {
|
|
const firstIssue = parseResult.error.issues[0]
|
|
return NextResponse.json(
|
|
{ success: false, error: firstIssue?.message || "Invalid input" },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const data = parseResult.data
|
|
const workos = getWorkOS()
|
|
|
|
// check if workos is configured (dev mode fallback)
|
|
const isConfigured =
|
|
process.env.WORKOS_API_KEY &&
|
|
process.env.WORKOS_CLIENT_ID &&
|
|
!process.env.WORKOS_API_KEY.includes("placeholder")
|
|
|
|
if (!isConfigured) {
|
|
return NextResponse.json({
|
|
success: true,
|
|
redirectUrl: "/dashboard",
|
|
devMode: true,
|
|
})
|
|
}
|
|
|
|
if (data.type === "password") {
|
|
const result = await workos.userManagement.authenticateWithPassword({
|
|
email: data.email,
|
|
password: data.password,
|
|
clientId: process.env.WORKOS_CLIENT_ID!,
|
|
})
|
|
|
|
// sync user to our database
|
|
await ensureUserExists({
|
|
id: result.user.id,
|
|
email: result.user.email,
|
|
firstName: result.user.firstName,
|
|
lastName: result.user.lastName,
|
|
profilePictureUrl: result.user.profilePictureUrl,
|
|
})
|
|
|
|
// save session with BOTH access and refresh tokens (fixes 30-second logout)
|
|
await saveSession(
|
|
{
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
user: result.user,
|
|
impersonator: result.impersonator,
|
|
},
|
|
request
|
|
)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
redirectUrl: "/dashboard",
|
|
})
|
|
}
|
|
|
|
if (data.type === "passwordless_send") {
|
|
const magicAuth = await workos.userManagement.createMagicAuth({
|
|
email: data.email,
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
magicAuthId: magicAuth.id,
|
|
})
|
|
}
|
|
|
|
if (data.type === "passwordless_verify") {
|
|
const result = await workos.userManagement.authenticateWithMagicAuth({
|
|
code: data.code,
|
|
email: data.email,
|
|
clientId: process.env.WORKOS_CLIENT_ID!,
|
|
})
|
|
|
|
// sync user to our database
|
|
await ensureUserExists({
|
|
id: result.user.id,
|
|
email: result.user.email,
|
|
firstName: result.user.firstName,
|
|
lastName: result.user.lastName,
|
|
profilePictureUrl: result.user.profilePictureUrl,
|
|
})
|
|
|
|
// save session with BOTH access and refresh tokens
|
|
await saveSession(
|
|
{
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
user: result.user,
|
|
impersonator: result.impersonator,
|
|
},
|
|
request
|
|
)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
redirectUrl: "/dashboard",
|
|
})
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ success: false, error: "Invalid login type" },
|
|
{ status: 400 }
|
|
)
|
|
} catch (error) {
|
|
console.error("Login error:", error)
|
|
return NextResponse.json(
|
|
{ success: false, error: mapWorkOSError(error) },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|