Skip to content

Replace custom server action guards with next-safe-action #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fe152fc
add: logging abstractions for better logging experience
ben-fornefeld Mar 15, 2025
7a838d2
refactor: user-actions.ts / components + setup action clients
ben-fornefeld Mar 15, 2025
4321d92
improve: user update error handling
ben-fornefeld Mar 15, 2025
cd68b74
refactor: auth-actions to safe action client
ben-fornefeld Mar 15, 2025
3b55e62
fix: auth tests
ben-fornefeld Mar 15, 2025
5b10c07
improve: auth flow, components + refactor: tests
ben-fornefeld Mar 15, 2025
331779b
refactor: templates-actions to action client
ben-fornefeld Mar 16, 2025
5519182
refactor: team-actions to safe action client
ben-fornefeld Mar 16, 2025
6a6a9ee
refactor: get-team-members to action client
ben-fornefeld Mar 16, 2025
6acc7c1
fix: build errors
ben-fornefeld Mar 16, 2025
f370311
refactor: sandboxes & templates server functions & error handling
ben-fornefeld Mar 16, 2025
1f2cfec
refactor: extract action utils & layout ui
ben-fornefeld Mar 16, 2025
2ffda65
refactor: billing actions to action client & improve: ui
ben-fornefeld Mar 17, 2025
26495e5
refactor: keys actions to action client
ben-fornefeld Mar 17, 2025
3c9e445
improve: disable sentry in development
ben-fornefeld Mar 17, 2025
e1ccec5
refactor: usage data fetching
ben-fornefeld Mar 17, 2025
5fc7b9d
remove: guard abstraction & tanstack query in favor of next-safe-action
ben-fornefeld Mar 17, 2025
d47c49b
improve: drop shadows to look the same as normal shadows
ben-fornefeld Mar 17, 2025
e3c3632
improve: toast ui & consistency
ben-fornefeld Mar 17, 2025
59ebcf0
improve: prose
ben-fornefeld Mar 17, 2025
2002c6e
fix: change password response handling
ben-fornefeld Mar 17, 2025
2627877
fix: network banner layouting
ben-fornefeld Mar 17, 2025
070f2d0
refactor: user & teams fetching & remove: swr package
ben-fornefeld Mar 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 158 additions & 129 deletions bun.lock

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"test:integration": "bun scripts:check-app-env && vitest run src/__test__/integration/",
"test:e2e": "bun scripts:check-all-env && vitest run src/__test__/e2e/",
"test:watch": "bun scripts:check-all-env && vitest",
"test:ui": "bun scripts:check-all-env && vitest --ui"
"test:ui": "bun scripts:check-all-env && vitest --ui",
"test:ui:integration": "bun scripts:check-app-env && vitest --ui src/__test__/integration/"
},
"dependencies": {
"@fumadocs/mdx-remote": "^1.2.0",
Expand Down Expand Up @@ -62,12 +63,12 @@
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.48.1",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.65.0",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.3",
"@theguild/remark-mermaid": "^0.2.0",
"@types/mdx": "^2.0.13",
"@vercel/kv": "^3.0.0",
"ansis": "^3.17.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
Expand All @@ -82,8 +83,9 @@
"lucide-react": "^0.474.0",
"motion": "^12.0.6",
"nanoid": "^5.0.9",
"next": "^15.2.2-canary.6",
"next": "^15.3.0-canary.10",
"next-logger": "^5.0.1",
"next-safe-action": "^7.10.4",
"next-themes": "^0.4.4",
"pg": "^8.14.0",
"pino": "^9.6.0",
Expand All @@ -103,11 +105,11 @@
"remark-math": "^6.0.0",
"remark-mermaid": "^0.2.0",
"shiki": "^2.3.2",
"swr": "^2.3.0",
"tailwind-merge": "^2.6.0",
"usehooks-ts": "^3.1.0",
"vaul": "^1.1.2",
"zod": "^3.24.1",
"zod-form-data": "^2.0.7",
"zustand": "^5.0.3",
"zustand-computed": "^2.0.2"
},
Expand Down
3 changes: 2 additions & 1 deletion sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ Sentry.init({
debug: false,

// Disable source maps in development to prevent 404 errors
attachStacktrace: process.env.NODE_ENV !== 'development',
attachStacktrace: process.env.NODE_ENV === 'production',
enabled: process.env.NODE_ENV === 'production',
})
3 changes: 2 additions & 1 deletion sentry.edge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ Sentry.init({
debug: false,

// Disable source maps in development to prevent 404 errors
attachStacktrace: process.env.NODE_ENV !== 'development',
attachStacktrace: process.env.NODE_ENV === 'production',
enabled: process.env.NODE_ENV === 'production',
})
3 changes: 2 additions & 1 deletion sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ Sentry.init({
debug: false,

// Disable source maps in development to prevent 404 errors
attachStacktrace: process.env.NODE_ENV !== 'development',
attachStacktrace: process.env.NODE_ENV === 'production',
enabled: process.env.NODE_ENV === 'production',
})
163 changes: 52 additions & 111 deletions src/__test__/integration/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import {
signInAction,
signUpAction,
forgotPasswordAction,
resetPasswordAction,
signInWithOAuth,
signOutAction,
signInWithOAuthAction,
} from '@/server/auth/auth-actions'
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { redirect } from 'next/navigation'
Expand All @@ -32,6 +31,12 @@ vi.mock('@/lib/clients/supabase/server', () => ({
createClient: vi.fn(() => mockSupabaseClient),
}))

vi.mock('@/lib/clients/supabase/admin', () => ({
supabaseAdmin: {
auth: vi.fn(),
},
}))

vi.mock('next/headers', () => ({
headers: vi.fn(() => ({
get: vi.fn((key) => {
Expand All @@ -54,12 +59,6 @@ vi.mock('@/lib/utils/auth', () => ({
})),
}))

vi.mock('@/lib/clients/logger', () => ({
logger: {
error: vi.fn(),
},
}))

describe('Auth Actions - Integration Tests', () => {
beforeEach(() => {
vi.resetAllMocks()
Expand Down Expand Up @@ -136,15 +135,11 @@ describe('Auth Actions - Integration Tests', () => {
formData.append('password', 'wrongpassword')

// Execute: Call the sign-in action
await signInAction(formData)
const result = await signInAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.SIGN_IN,
'Invalid login credentials',
{ returnTo: '' }
)
expect(result).toBeDefined()
expect(result).toHaveProperty('serverError')
})
})

Expand All @@ -167,15 +162,12 @@ describe('Auth Actions - Integration Tests', () => {
formData.append('confirmPassword', 'Password123!')

// Execute: Call the sign-up action
await signUpAction(formData)
const result = await signUpAction(formData)

// Verify: Check that encodedRedirect was called with success message
expect(encodedRedirect).toHaveBeenCalledWith(
'success',
AUTH_URLS.SIGN_UP,
'Thanks for signing up! Please check your email for a verification link.',
{ returnTo: '' }
)
expect(result).toBeDefined()
expect(result).not.toHaveProperty('serverError')
expect(result).not.toHaveProperty('validationErrors')
})

/**
Expand All @@ -190,15 +182,11 @@ describe('Auth Actions - Integration Tests', () => {
formData.append('confirmPassword', 'DifferentPassword!')

// Execute: Call the sign-up action
await signUpAction(formData)
const result = await signUpAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.SIGN_UP,
'Passwords do not match',
{ returnTo: '' }
)
expect(result).toBeDefined()
expect(result).toHaveProperty('validationErrors')
})

/**
Expand All @@ -212,15 +200,11 @@ describe('Auth Actions - Integration Tests', () => {
// Missing password and confirmPassword

// Execute: Call the sign-up action
await signUpAction(formData)
const result = await signUpAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.SIGN_UP,
'E-Mail and both passwords are required',
{ returnTo: '' }
)
// Verify: Check that the result contains validation errors
expect(result).toBeDefined()
expect(result).toHaveProperty('validationErrors')
})

/**
Expand All @@ -247,20 +231,11 @@ describe('Auth Actions - Integration Tests', () => {
formData.append('confirmPassword', 'Password123!')

// Execute: Call the sign-up action
await signUpAction(formData)
const result = await signUpAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.SIGN_UP,
'User already registered',
{ returnTo: '' }
)

// Verify: console.error should have been called
expect(console.error).toHaveBeenCalledWith(
'auth/user-already-exists User already registered'
)
expect(result).toBeDefined()
expect(result).toHaveProperty('serverError')
})
})

Expand All @@ -281,15 +256,12 @@ describe('Auth Actions - Integration Tests', () => {
formData.append('email', 'user@example.com')

// Execute: Call the forgot password action
await forgotPasswordAction(formData)
const result = await forgotPasswordAction(formData)

// Verify: Check that encodedRedirect was called with success message
expect(encodedRedirect).toHaveBeenCalledWith(
'success',
AUTH_URLS.FORGOT_PASSWORD,
'Check your email for a link to reset your password.',
{ type: 'reset_password' }
)
expect(result).toBeDefined()
expect(result).not.toHaveProperty('serverError')
expect(result).not.toHaveProperty('validationErrors')
})

/**
Expand All @@ -301,74 +273,40 @@ describe('Auth Actions - Integration Tests', () => {
const formData = new FormData()

// Execute: Call the forgot password action
await forgotPasswordAction(formData)
const result = await forgotPasswordAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.FORGOT_PASSWORD,
'E-Mail is required'
)
expect(result).toBeDefined()
expect(result).toHaveProperty('validationErrors')
})

// TODO: find a way to fix authActionClient actions
/**
* AUTHENTICATION TEST: Verifies that reset password with valid data
* shows success message
*/
it('should show success message on valid password reset', async () => {
/* it('should show success message on valid password reset', async () => {
// Setup: Mock Supabase client to return successful password update
mockSupabaseClient.auth.updateUser.mockResolvedValue({
data: { user: { id: 'user-123' } },
error: null,
})

// Setup: Create form data with valid password reset data
const formData = new FormData()
formData.append('password', 'NewPassword123!')
formData.append('confirmPassword', 'NewPassword123!')

// Execute: Call the reset password action
await resetPasswordAction(formData)

// Verify: Check that encodedRedirect was called with success message
expect(encodedRedirect).toHaveBeenCalledWith(
'success',
AUTH_URLS.RESET_PASSWORD,
'Password updated'
)
})
// Mock the context with supabase client that would be provided by authActionClient
const mockCtx = {
supabase: mockSupabaseClient,
user: { id: 'user-123' },
}

/**
* VALIDATION TEST: Verifies that reset password with mismatched passwords
* shows appropriate error message
*/
it('should show error when passwords do not match for reset', async () => {
// Setup: Mock Supabase client to return a value for updateUser
// This is needed because the resetPasswordAction function doesn't have proper return statements
// and continues executing even after the password mismatch check
mockSupabaseClient.auth.updateUser.mockResolvedValue({
data: { user: null },
error: null,
// Execute: Call the updateUser action with mocked context
const result = await updateUserAction.implementation({
parsedInput: { password: 'NewPassword123!' },
ctx: mockCtx,
})

// Setup: Create form data with mismatched passwords
const formData = new FormData()
formData.append('password', 'NewPassword123!')
formData.append('confirmPassword', 'DifferentPassword!')

// Execute: Call the reset password action
await resetPasswordAction(formData)

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
'error',
AUTH_URLS.RESET_PASSWORD,
'Passwords do not match'
)

// Verify: updateUser should not be called because passwords don't match
expect(mockSupabaseClient.auth.updateUser).not.toHaveBeenCalled()
})
// Verify: Check that the action returned the expected result
expect(result).toBeDefined()
expect(result).toHaveProperty('user')
}) */
})

describe('OAuth Authentication', () => {
Expand All @@ -383,7 +321,7 @@ describe('Auth Actions - Integration Tests', () => {
})

// Execute: Call the OAuth sign-in action
await signInWithOAuth('github')
await signInWithOAuthAction({ provider: 'github' })

// Verify: Check that redirect was called with OAuth URL
expect(redirect).toHaveBeenCalledWith('https://oauth-provider.com/auth')
Expand All @@ -401,7 +339,7 @@ describe('Auth Actions - Integration Tests', () => {
})

// Execute: Call the OAuth sign-in action
await signInWithOAuth('github')
await signInWithOAuthAction({ provider: 'github' })

// Verify: Check that encodedRedirect was called with error message
expect(encodedRedirect).toHaveBeenCalledWith(
Expand All @@ -424,7 +362,10 @@ describe('Auth Actions - Integration Tests', () => {
})

// Execute: Call the OAuth sign-in action with returnTo
await signInWithOAuth('github', '/dashboard/team-123')
await signInWithOAuthAction({
provider: 'github',
returnTo: '/dashboard/team-123',
})

// Verify: Check that signInWithOAuth was called with correct options
expect(mockSupabaseClient.auth.signInWithOAuth).toHaveBeenCalledWith({
Expand Down
17 changes: 15 additions & 2 deletions src/__test__/setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { loadEnvConfig } from '@next/env'
import { vi } from 'vitest'

vi.stubEnv('NODE_ENV', 'test')

// load env variables
const projectDir = process.cwd()
loadEnvConfig(projectDir)

// default mocks
vi.mock('@/lib/clients/logger', () => ({
logger: {
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
logError: vi.fn(),
logInfo: vi.fn(),
logWarn: vi.fn(),
logDebug: vi.fn(),
}))
Loading