compassmock/src/app/actions/profile.ts
Nicholai a0dd50f59b
feat(auth): add user profiles and improve auth security (#33)
- 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>
2026-02-05 08:20:51 -07:00

134 lines
3.3 KiB
TypeScript
Executable File

"use server"
import { getWorkOS, signOut } from "@workos-inc/authkit-nextjs"
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"
import { requireAuth } from "@/lib/auth"
import {
updateProfileSchema,
changePasswordSchema,
type UpdateProfileInput,
type ChangePasswordInput,
} from "@/lib/validations/profile"
type ActionResult<T = undefined> =
| { success: true; data?: T }
| { success: false; error: string }
/**
* Update the current user's profile (first name, last name)
*/
export async function updateProfile(
input: UpdateProfileInput
): Promise<ActionResult> {
try {
// Validate input
const parsed = updateProfileSchema.safeParse(input)
if (!parsed.success) {
return {
success: false,
error: parsed.error.issues[0]?.message ?? "Invalid input",
}
}
const { firstName, lastName } = parsed.data
// Get current authenticated user
const currentUser = await requireAuth()
// Update in WorkOS
const workos = getWorkOS()
await workos.userManagement.updateUser({
userId: currentUser.id,
firstName,
lastName,
})
// Update in local database
const { env } = await getCloudflareContext()
if (env?.DB) {
const db = getDb(env.DB)
const now = new Date().toISOString()
const displayName = `${firstName} ${lastName}`.trim()
await db
.update(users)
.set({
firstName,
lastName,
displayName,
updatedAt: now,
})
.where(eq(users.id, currentUser.id))
.run()
}
return { success: true }
} catch (error) {
console.error("Error updating profile:", error)
return {
success: false,
error: error instanceof Error ? error.message : "Failed to update profile",
}
}
}
/**
* Change the current user's password
* Note: WorkOS doesn't verify the current password via API - this is a UX-only field.
* For production, consider implementing a proper password verification flow.
*/
export async function changePassword(
input: ChangePasswordInput
): Promise<ActionResult> {
try {
// Validate input
const parsed = changePasswordSchema.safeParse(input)
if (!parsed.success) {
return {
success: false,
error: parsed.error.issues[0]?.message ?? "Invalid input",
}
}
const { newPassword } = parsed.data
// Get current authenticated user
const currentUser = await requireAuth()
// Update password in WorkOS
const workos = getWorkOS()
await workos.userManagement.updateUser({
userId: currentUser.id,
password: newPassword,
})
return { success: true }
} catch (error) {
console.error("Error changing password:", error)
// Handle specific WorkOS errors
const errorMessage =
error instanceof Error ? error.message : "Failed to change password"
// Check for common error patterns
if (errorMessage.includes("password")) {
return {
success: false,
error: "Unable to change password. You may have signed in with a social provider.",
}
}
return { success: false, error: errorMessage }
}
}
/**
* Sign out the current user
*/
export async function logout(): Promise<void> {
await signOut()
}