Skip to content
Merged
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