Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"sideEffects": false,
"repository": "https://github.com/TanStack/tanstack.com.git",
"packageManager": "pnpm@9.4.0",
"packageManager": "pnpm@10.26.0",
"type": "module",
"scripts": {
"dev": "pnpm run with-env vite dev",
Expand Down Expand Up @@ -59,12 +59,12 @@
"hast-util-to-string": "^3.0.1",
"html-react-parser": "^5.1.10",
"lru-cache": "^7.13.1",
"lucide-react": "^0.561.0",
"mermaid": "^11.11.0",
"postgres": "^3.4.7",
"react": "^19.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.0",
"react-icons": "^5.3.0",
"react-instantsearch": "7",
"rehype-autolink-headings": "^7.1.0",
"rehype-callouts": "^2.1.2",
Expand Down
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

167 changes: 167 additions & 0 deletions src/auth/auth.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Auth Service
*
* Main authentication service that coordinates session validation
* and user retrieval. Uses inversion of control for all dependencies.
*/

import type {
AuthUser,
Capability,
DbUser,
IAuthService,
ICapabilitiesRepository,
ISessionService,
IUserRepository,
SessionCookieData,
} from './types'
import { AuthError } from './types'

// ============================================================================
// Auth Service Implementation
// ============================================================================

export class AuthService implements IAuthService {
constructor(
private sessionService: ISessionService,
private userRepository: IUserRepository,
private capabilitiesRepository: ICapabilitiesRepository,
) {}

/**
* Get current user from request
* Returns null if not authenticated
*/
async getCurrentUser(request: Request): Promise<AuthUser | null> {
const signedCookie = this.sessionService.getSessionCookie(request)

if (!signedCookie) {
return null
}

try {
const cookieData = await this.sessionService.verifyCookie(signedCookie)

if (!cookieData) {
console.error(
'[AuthService] Session cookie verification failed - invalid signature or expired',
)
return null
}

const result = await this.validateSession(cookieData)
if (!result) {
return null
}

return this.mapDbUserToAuthUser(result.user, result.capabilities)
} catch (error) {
console.error('[AuthService] Failed to get user from session:', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
})
return null
}
}

/**
* Validate session data against the database
*/
async validateSession(
sessionData: SessionCookieData,
): Promise<{ user: DbUser; capabilities: Capability[] } | null> {
const user = await this.userRepository.findById(sessionData.userId)

if (!user) {
console.error(
`[AuthService] Session cookie references non-existent user ${sessionData.userId}`,
)
return null
}

// Verify session version matches (for session revocation)
if (user.sessionVersion !== sessionData.version) {
console.error(
`[AuthService] Session version mismatch for user ${user.id} - expected ${user.sessionVersion}, got ${sessionData.version}`,
)
return null
}

// Get effective capabilities
const capabilities =
await this.capabilitiesRepository.getEffectiveCapabilities(user.id)

return { user, capabilities }
}

/**
* Map database user to AuthUser type
*/
private mapDbUserToAuthUser(
user: DbUser,
capabilities: Capability[],
): AuthUser {
return {
userId: user.id,
email: user.email,
name: user.name,
image: user.image,
displayUsername: user.displayUsername,
capabilities,
adsDisabled: user.adsDisabled,
interestedInHidingAds: user.interestedInHidingAds,
}
}
}

// ============================================================================
// Auth Guard Functions
// ============================================================================

/**
* Require authentication - throws if not authenticated
*/
export async function requireAuthentication(
authService: IAuthService,
request: Request,
): Promise<AuthUser> {
const user = await authService.getCurrentUser(request)
if (!user) {
throw new AuthError('Not authenticated', 'NOT_AUTHENTICATED')
}
return user
}

/**
* Require specific capability - throws if not authorized
*/
export async function requireCapability(
authService: IAuthService,
request: Request,
capability: Capability,
): Promise<AuthUser> {
const user = await requireAuthentication(authService, request)

const hasAccess =
user.capabilities.includes('admin') ||
user.capabilities.includes(capability)

if (!hasAccess) {
throw new AuthError(
`Missing required capability: ${capability}`,
'MISSING_CAPABILITY',
)
}

return user
}

/**
* Require admin capability - throws if not admin
*/
export async function requireAdmin(
authService: IAuthService,
request: Request,
): Promise<AuthUser> {
return requireCapability(authService, request, 'admin')
}
101 changes: 101 additions & 0 deletions src/auth/capabilities.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Capabilities Service
*
* Handles authorization via capability-based access control.
* Uses inversion of control for data access.
*/

import type { Capability, ICapabilitiesRepository, AuthUser } from './types'

// ============================================================================
// Capabilities Service
// ============================================================================

export class CapabilitiesService {
constructor(private repository: ICapabilitiesRepository) {}

/**
* Get effective capabilities for a user (direct + role-based)
*/
async getEffectiveCapabilities(userId: string): Promise<Capability[]> {
return this.repository.getEffectiveCapabilities(userId)
}

/**
* Get effective capabilities for multiple users efficiently
*/
async getBulkEffectiveCapabilities(
userIds: string[],
): Promise<Record<string, Capability[]>> {
return this.repository.getBulkEffectiveCapabilities(userIds)
}
}

// ============================================================================
// Capability Checking Utilities
// ============================================================================

/**
* Check if user has a specific capability
* Admin users have access to all capabilities
*/
export function hasCapability(
capabilities: Capability[],
requiredCapability: Capability,
): boolean {
return (
capabilities.includes('admin') || capabilities.includes(requiredCapability)
)
}

/**
* Check if user has all specified capabilities
*/
export function hasAllCapabilities(
capabilities: Capability[],
requiredCapabilities: Capability[],
): boolean {
if (capabilities.includes('admin')) {
return true
}
return requiredCapabilities.every((cap) => capabilities.includes(cap))
}

/**
* Check if user has any of the specified capabilities
*/
export function hasAnyCapability(
capabilities: Capability[],
requiredCapabilities: Capability[],
): boolean {
if (capabilities.includes('admin')) {
return true
}
return requiredCapabilities.some((cap) => capabilities.includes(cap))
}

/**
* Check if user is admin
*/
export function isAdmin(capabilities: Capability[]): boolean {
return capabilities.includes('admin')
}

/**
* Check if AuthUser has a specific capability
*/
export function userHasCapability(
user: AuthUser | null | undefined,
capability: Capability,
): boolean {
if (!user) return false
return hasCapability(user.capabilities, capability)
}

/**
* Check if AuthUser is admin
*/
export function userIsAdmin(user: AuthUser | null | undefined): boolean {
if (!user) return false
return isAdmin(user.capabilities)
}
Loading
Loading