Skip to content

Commit 6011aef

Browse files
tannerlinsleyclaudeautofix-ci[bot]LadyBluenotes
authored
refactor: isolate auth module using inversion of control (#584)
* refactor: isolate auth module using inversion of control Create a standalone auth module at ~/auth that encapsulates all authentication logic, making it independent of the rest of the application through IoC interfaces. New auth module structure: - types.ts: Core types and IoC interfaces (IUserRepository, etc.) - session.server.ts: Cookie-based session management with HMAC-SHA256 - auth.server.ts: Main AuthService for user authentication - capabilities.server.ts: CapabilitiesService with helper utilities - oauth.server.ts: OAuthService and OAuth provider utilities - guards.server.ts: Auth guards factory and decorators - repositories.server.ts: Drizzle-based repository implementations - context.server.ts: Dependency injection/service composition root - index.server.ts: Server-side public API exports - index.ts: Client-side public API exports - client.ts: Client-side auth utilities The existing utils files now delegate to the new auth module for backward compatibility. Auth routes updated to use the new module directly. * ci: apply automated fixes * Site Updates (#583) * book icon update * fix book icon * update command icon * replace react-icons/bs * migrate to close icons to lucide-react x * discord, close, and github icons plus some others * update icons to lucide-react for improved consistency * replace FaBolt with Zap icon in multiple components for consistency * replace FontAwesome icons with Lucide icons for consistency * add CheckCircleIcon component and replace FaCheckCircle usage for consistency * remove unused FontAwesome icons for improved consistency * replace FaCogs with CogsIcon component for consistency across multiple files * replace FaComment with MessageSquare icon for consistency across multiple components * replace FontAwesome icons with Lucide icons for consistency across multiple components * replace FaExternalLinkAlt and FaEdit with ExternalLink and SquarePen icons for consistency across multiple components * replace FontAwesome icons with Lucide icons for consistency across multiple components * replace icons with Lucide icons for consistency across multiple components * replace icons with Lucide icons for consistency across multiple components * auth logs * update icons * add BrandXIcon and BSkyIcon components; update Navbar to use new icons * replace Material Design icons with Lucide icons for consistency across multiple components * replace react-icons with Lucide icons * replace react-icons with Lucide icons * ci: apply automated fixes * fix broken icon * enable lazy loading and async decoding for images in Markdown component --------- Co-authored-by: Tanner Linsley <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * Fix conflicts * Fixes * ci: apply automated fixes * icon * refactor: isolate auth module using inversion of control Create a standalone auth module at ~/auth that encapsulates all authentication logic, making it independent of the rest of the application through IoC interfaces. New auth module structure: - types.ts: Core types and IoC interfaces (IUserRepository, etc.) - session.server.ts: Cookie-based session management with HMAC-SHA256 - auth.server.ts: Main AuthService for user authentication - capabilities.server.ts: CapabilitiesService with helper utilities - oauth.server.ts: OAuthService and OAuth provider utilities - guards.server.ts: Auth guards factory and decorators - repositories.server.ts: Drizzle-based repository implementations - context.server.ts: Dependency injection/service composition root - index.server.ts: Server-side public API exports - index.ts: Client-side public API exports - client.ts: Client-side auth utilities The existing utils files now delegate to the new auth module for backward compatibility. Auth routes updated to use the new module directly. * ci: apply automated fixes * checkpoint --------- Co-authored-by: Claude <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Sarah <[email protected]>
1 parent 7d6208f commit 6011aef

20 files changed

+2266
-801
lines changed

src/auth/auth.server.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Auth Service
3+
*
4+
* Main authentication service that coordinates session validation
5+
* and user retrieval. Uses inversion of control for all dependencies.
6+
*/
7+
8+
import type {
9+
AuthUser,
10+
Capability,
11+
DbUser,
12+
IAuthService,
13+
ICapabilitiesRepository,
14+
ISessionService,
15+
IUserRepository,
16+
SessionCookieData,
17+
} from './types'
18+
import { AuthError } from './types'
19+
20+
// ============================================================================
21+
// Auth Service Implementation
22+
// ============================================================================
23+
24+
export class AuthService implements IAuthService {
25+
constructor(
26+
private sessionService: ISessionService,
27+
private userRepository: IUserRepository,
28+
private capabilitiesRepository: ICapabilitiesRepository,
29+
) {}
30+
31+
/**
32+
* Get current user from request
33+
* Returns null if not authenticated
34+
*/
35+
async getCurrentUser(request: Request): Promise<AuthUser | null> {
36+
const signedCookie = this.sessionService.getSessionCookie(request)
37+
38+
if (!signedCookie) {
39+
return null
40+
}
41+
42+
try {
43+
const cookieData = await this.sessionService.verifyCookie(signedCookie)
44+
45+
if (!cookieData) {
46+
console.error(
47+
'[AuthService] Session cookie verification failed - invalid signature or expired',
48+
)
49+
return null
50+
}
51+
52+
const result = await this.validateSession(cookieData)
53+
if (!result) {
54+
return null
55+
}
56+
57+
return this.mapDbUserToAuthUser(result.user, result.capabilities)
58+
} catch (error) {
59+
console.error('[AuthService] Failed to get user from session:', {
60+
error: error instanceof Error ? error.message : 'Unknown error',
61+
stack: error instanceof Error ? error.stack : undefined,
62+
})
63+
return null
64+
}
65+
}
66+
67+
/**
68+
* Validate session data against the database
69+
*/
70+
async validateSession(
71+
sessionData: SessionCookieData,
72+
): Promise<{ user: DbUser; capabilities: Capability[] } | null> {
73+
const user = await this.userRepository.findById(sessionData.userId)
74+
75+
if (!user) {
76+
console.error(
77+
`[AuthService] Session cookie references non-existent user ${sessionData.userId}`,
78+
)
79+
return null
80+
}
81+
82+
// Verify session version matches (for session revocation)
83+
if (user.sessionVersion !== sessionData.version) {
84+
console.error(
85+
`[AuthService] Session version mismatch for user ${user.id} - expected ${user.sessionVersion}, got ${sessionData.version}`,
86+
)
87+
return null
88+
}
89+
90+
// Get effective capabilities
91+
const capabilities =
92+
await this.capabilitiesRepository.getEffectiveCapabilities(user.id)
93+
94+
return { user, capabilities }
95+
}
96+
97+
/**
98+
* Map database user to AuthUser type
99+
*/
100+
private mapDbUserToAuthUser(
101+
user: DbUser,
102+
capabilities: Capability[],
103+
): AuthUser {
104+
return {
105+
userId: user.id,
106+
email: user.email,
107+
name: user.name,
108+
image: user.image,
109+
displayUsername: user.displayUsername,
110+
capabilities,
111+
adsDisabled: user.adsDisabled,
112+
interestedInHidingAds: user.interestedInHidingAds,
113+
}
114+
}
115+
}
116+
117+
// ============================================================================
118+
// Auth Guard Functions
119+
// ============================================================================
120+
121+
/**
122+
* Require authentication - throws if not authenticated
123+
*/
124+
export async function requireAuthentication(
125+
authService: IAuthService,
126+
request: Request,
127+
): Promise<AuthUser> {
128+
const user = await authService.getCurrentUser(request)
129+
if (!user) {
130+
throw new AuthError('Not authenticated', 'NOT_AUTHENTICATED')
131+
}
132+
return user
133+
}
134+
135+
/**
136+
* Require specific capability - throws if not authorized
137+
*/
138+
export async function requireCapability(
139+
authService: IAuthService,
140+
request: Request,
141+
capability: Capability,
142+
): Promise<AuthUser> {
143+
const user = await requireAuthentication(authService, request)
144+
145+
const hasAccess =
146+
user.capabilities.includes('admin') ||
147+
user.capabilities.includes(capability)
148+
149+
if (!hasAccess) {
150+
throw new AuthError(
151+
`Missing required capability: ${capability}`,
152+
'MISSING_CAPABILITY',
153+
)
154+
}
155+
156+
return user
157+
}
158+
159+
/**
160+
* Require admin capability - throws if not admin
161+
*/
162+
export async function requireAdmin(
163+
authService: IAuthService,
164+
request: Request,
165+
): Promise<AuthUser> {
166+
return requireCapability(authService, request, 'admin')
167+
}

src/auth/capabilities.server.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Capabilities Service
3+
*
4+
* Handles authorization via capability-based access control.
5+
* Uses inversion of control for data access.
6+
*/
7+
8+
import type { Capability, ICapabilitiesRepository, AuthUser } from './types'
9+
10+
// ============================================================================
11+
// Capabilities Service
12+
// ============================================================================
13+
14+
export class CapabilitiesService {
15+
constructor(private repository: ICapabilitiesRepository) {}
16+
17+
/**
18+
* Get effective capabilities for a user (direct + role-based)
19+
*/
20+
async getEffectiveCapabilities(userId: string): Promise<Capability[]> {
21+
return this.repository.getEffectiveCapabilities(userId)
22+
}
23+
24+
/**
25+
* Get effective capabilities for multiple users efficiently
26+
*/
27+
async getBulkEffectiveCapabilities(
28+
userIds: string[],
29+
): Promise<Record<string, Capability[]>> {
30+
return this.repository.getBulkEffectiveCapabilities(userIds)
31+
}
32+
}
33+
34+
// ============================================================================
35+
// Capability Checking Utilities
36+
// ============================================================================
37+
38+
/**
39+
* Check if user has a specific capability
40+
* Admin users have access to all capabilities
41+
*/
42+
export function hasCapability(
43+
capabilities: Capability[],
44+
requiredCapability: Capability,
45+
): boolean {
46+
return (
47+
capabilities.includes('admin') || capabilities.includes(requiredCapability)
48+
)
49+
}
50+
51+
/**
52+
* Check if user has all specified capabilities
53+
*/
54+
export function hasAllCapabilities(
55+
capabilities: Capability[],
56+
requiredCapabilities: Capability[],
57+
): boolean {
58+
if (capabilities.includes('admin')) {
59+
return true
60+
}
61+
return requiredCapabilities.every((cap) => capabilities.includes(cap))
62+
}
63+
64+
/**
65+
* Check if user has any of the specified capabilities
66+
*/
67+
export function hasAnyCapability(
68+
capabilities: Capability[],
69+
requiredCapabilities: Capability[],
70+
): boolean {
71+
if (capabilities.includes('admin')) {
72+
return true
73+
}
74+
return requiredCapabilities.some((cap) => capabilities.includes(cap))
75+
}
76+
77+
/**
78+
* Check if user is admin
79+
*/
80+
export function isAdmin(capabilities: Capability[]): boolean {
81+
return capabilities.includes('admin')
82+
}
83+
84+
/**
85+
* Check if AuthUser has a specific capability
86+
*/
87+
export function userHasCapability(
88+
user: AuthUser | null | undefined,
89+
capability: Capability,
90+
): boolean {
91+
if (!user) return false
92+
return hasCapability(user.capabilities, capability)
93+
}
94+
95+
/**
96+
* Check if AuthUser is admin
97+
*/
98+
export function userIsAdmin(user: AuthUser | null | undefined): boolean {
99+
if (!user) return false
100+
return isAdmin(user.capabilities)
101+
}

src/auth/client.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Auth Client Module
3+
*
4+
* Client-side authentication utilities and navigation helpers.
5+
* This module is safe to import in browser code.
6+
*/
7+
8+
import type { OAuthProvider } from './types'
9+
10+
// ============================================================================
11+
// Auth Client
12+
// ============================================================================
13+
14+
/**
15+
* Client-side auth utilities for OAuth flows
16+
*/
17+
export const authClient = {
18+
signIn: {
19+
/**
20+
* Initiate OAuth sign-in with a social provider
21+
*/
22+
social: ({ provider }: { provider: OAuthProvider }) => {
23+
window.location.href = `/auth/${provider}/start`
24+
},
25+
},
26+
27+
/**
28+
* Sign out the current user
29+
*/
30+
signOut: async () => {
31+
window.location.href = '/auth/signout'
32+
},
33+
}
34+
35+
// ============================================================================
36+
// Navigation Helpers
37+
// ============================================================================
38+
39+
/**
40+
* Navigate to sign-in page
41+
*/
42+
export function navigateToSignIn(
43+
provider?: OAuthProvider,
44+
returnTo?: string,
45+
): void {
46+
if (provider) {
47+
const url = returnTo
48+
? `/auth/${provider}/start?returnTo=${encodeURIComponent(returnTo)}`
49+
: `/auth/${provider}/start`
50+
window.location.href = url
51+
} else {
52+
const url = returnTo
53+
? `/login?returnTo=${encodeURIComponent(returnTo)}`
54+
: '/login'
55+
window.location.href = url
56+
}
57+
}
58+
59+
/**
60+
* Navigate to sign-out
61+
*/
62+
export function navigateToSignOut(): void {
63+
window.location.href = '/auth/signout'
64+
}
65+
66+
/**
67+
* Get current URL path for return-to parameter
68+
*/
69+
export function getCurrentPath(): string {
70+
return window.location.pathname + window.location.search
71+
}

0 commit comments

Comments
 (0)