feat(#4): Implement Authentik OIDC authentication with BetterAuth
- Integrated BetterAuth library for modern authentication - Added Session, Account, and Verification database tables - Created complete auth module with service, controller, guards, and decorators - Implemented shared authentication types in @mosaic/shared package - Added comprehensive test coverage (26 tests passing) - Documented type sharing strategy for monorepo - Updated environment configuration with OIDC and JWT settings Key architectural decisions: - BetterAuth over Passport.js for better TypeScript support - Separation of User (DB entity) vs AuthUser (client-safe subset) - Shared types package to prevent FE/BE drift - Factory pattern for auth config to use shared Prisma instance Ready for frontend integration (Issue #6). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Fixes #4
This commit is contained in:
287
docs/TYPE-SHARING.md
Normal file
287
docs/TYPE-SHARING.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Type Sharing Strategy
|
||||
|
||||
This document explains how types are shared between the frontend and backend in the Mosaic Stack monorepo.
|
||||
|
||||
## Overview
|
||||
|
||||
All types that are used by both frontend and backend live in the `@mosaic/shared` package. This ensures:
|
||||
- **Type safety** across the entire stack
|
||||
- **Single source of truth** for data structures
|
||||
- **Automatic type updates** when the API changes
|
||||
- **Reduced duplication** and maintenance burden
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
packages/shared/
|
||||
├── src/
|
||||
│ ├── types/
|
||||
│ │ ├── index.ts # Main export file
|
||||
│ │ ├── enums.ts # Shared enums (TaskStatus, etc.)
|
||||
│ │ ├── database.types.ts # Database entity types
|
||||
│ │ └── auth.types.ts # Authentication types (NEW)
|
||||
│ ├── utils/
|
||||
│ └── constants.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Authentication Types
|
||||
|
||||
### Shared Types (`@mosaic/shared`)
|
||||
|
||||
These types are used by **both** frontend and backend:
|
||||
|
||||
#### `AuthUser`
|
||||
The authenticated user object that's safe to expose to clients.
|
||||
```typescript
|
||||
interface AuthUser {
|
||||
readonly id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
emailVerified?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### `AuthSession`
|
||||
Session data returned after successful authentication.
|
||||
```typescript
|
||||
interface AuthSession {
|
||||
user: AuthUser;
|
||||
session: {
|
||||
id: string;
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### `Session`, `Account`
|
||||
Full database entity types for sessions and OAuth accounts.
|
||||
|
||||
#### `LoginRequest`, `LoginResponse`
|
||||
Request/response payloads for authentication endpoints.
|
||||
|
||||
#### `OAuthProvider`
|
||||
Supported OAuth providers: `"authentik" | "google" | "github"`
|
||||
|
||||
#### `OAuthCallbackParams`
|
||||
Query parameters from OAuth callback redirects.
|
||||
|
||||
### Backend-Only Types
|
||||
|
||||
Types that are only used by the backend stay in `apps/api/src/auth/types/`:
|
||||
|
||||
#### `BetterAuthRequest`
|
||||
Internal type for BetterAuth handler compatibility (extends web standard `Request`).
|
||||
|
||||
**Why backend-only?** This is an implementation detail of how NestJS integrates with BetterAuth. The frontend doesn't need to know about it.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### In the Backend (API)
|
||||
|
||||
```typescript
|
||||
// apps/api/src/auth/auth.controller.ts
|
||||
import { AuthUser } from "@mosaic/shared";
|
||||
|
||||
@Get("profile")
|
||||
@UseGuards(AuthGuard)
|
||||
getProfile(@CurrentUser() user: AuthUser) {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### In the Frontend (Web)
|
||||
|
||||
```typescript
|
||||
// apps/web/app/components/UserProfile.tsx
|
||||
import type { AuthUser } from "@mosaic/shared";
|
||||
|
||||
export function UserProfile({ user }: { user: AuthUser }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>{user.name}</h1>
|
||||
<p>{user.email}</p>
|
||||
{user.emailVerified && <Badge>Verified</Badge>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### API Response Types
|
||||
|
||||
```typescript
|
||||
// Both FE and BE can use this
|
||||
import type { ApiResponse, AuthSession } from "@mosaic/shared";
|
||||
|
||||
// Backend returns this
|
||||
const response: ApiResponse<AuthSession> = {
|
||||
success: true,
|
||||
data: {
|
||||
user: { id: "...", email: "...", name: "..." },
|
||||
session: { id: "...", token: "...", expiresAt: new Date() },
|
||||
},
|
||||
};
|
||||
|
||||
// Frontend consumes this
|
||||
async function login(email: string, password: string) {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data: ApiResponse<AuthSession> = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// TypeScript knows data.data exists and is AuthSession
|
||||
saveSession(data.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Types
|
||||
|
||||
The `@mosaic/shared` package also exports database entity types that match the Prisma schema:
|
||||
|
||||
- `User` - Full user entity (includes all fields from DB)
|
||||
- `Workspace`, `Task`, `Event`, `Project` - Other entities
|
||||
- Enums: `TaskStatus`, `TaskPriority`, `ProjectStatus`, etc.
|
||||
|
||||
### Key Difference: `User` vs `AuthUser`
|
||||
|
||||
| Type | Purpose | Fields | Used By |
|
||||
|------|---------|--------|---------|
|
||||
| `User` | Full database entity | All DB fields including sensitive data | Backend internal logic |
|
||||
| `AuthUser` | Safe client-exposed subset | Only public fields (no preferences, etc.) | API responses, Frontend |
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// Backend internal logic
|
||||
import { User } from "@mosaic/shared";
|
||||
|
||||
async function updateUserPreferences(userId: string, prefs: User["preferences"]) {
|
||||
// Has access to all fields including preferences
|
||||
}
|
||||
|
||||
// API response
|
||||
import { AuthUser } from "@mosaic/shared";
|
||||
|
||||
function sanitizeUser(user: User): AuthUser {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
emailVerified: user.emailVerified,
|
||||
};
|
||||
// Preferences and other sensitive fields are excluded
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Shared Types
|
||||
|
||||
When adding new types that should be shared:
|
||||
|
||||
1. **Determine if it should be shared:**
|
||||
- ✅ API request/response payloads
|
||||
- ✅ Database entity shapes
|
||||
- ✅ Business domain types
|
||||
- ✅ Enums and constants
|
||||
- ❌ Backend-only implementation details
|
||||
- ❌ Frontend-only UI component props
|
||||
|
||||
2. **Add to the appropriate file:**
|
||||
- Database entities → `database.types.ts`
|
||||
- Auth-related → `auth.types.ts`
|
||||
- Enums → `enums.ts`
|
||||
- General API types → `index.ts`
|
||||
|
||||
3. **Export from `index.ts`:**
|
||||
```typescript
|
||||
export * from "./your-new-types";
|
||||
```
|
||||
|
||||
4. **Build the shared package:**
|
||||
```bash
|
||||
cd packages/shared
|
||||
pnpm build
|
||||
```
|
||||
|
||||
5. **Use in your apps:**
|
||||
```typescript
|
||||
import { YourType } from "@mosaic/shared";
|
||||
```
|
||||
|
||||
## Type Versioning
|
||||
|
||||
Since this is a monorepo, all packages use the same version of `@mosaic/shared`. When you:
|
||||
|
||||
1. **Change a shared type** - Both FE and BE automatically get the update
|
||||
2. **Add a new field** - TypeScript will show errors where the field is missing
|
||||
3. **Remove a field** - TypeScript will show errors where the field is still used
|
||||
|
||||
This ensures the frontend and backend never drift out of sync.
|
||||
|
||||
## Benefits
|
||||
|
||||
### Type Safety
|
||||
```typescript
|
||||
// If the backend changes AuthUser.name to AuthUser.displayName,
|
||||
// the frontend will get TypeScript errors everywhere AuthUser is used.
|
||||
// You'll fix all uses before deploying.
|
||||
```
|
||||
|
||||
### Auto-Complete
|
||||
```typescript
|
||||
// Frontend developers get full autocomplete for API types
|
||||
const user: AuthUser = await fetchUser();
|
||||
user. // <-- IDE shows: id, email, name, image, emailVerified
|
||||
```
|
||||
|
||||
### Refactoring
|
||||
```typescript
|
||||
// Rename a field? TypeScript finds all usages across FE and BE
|
||||
// No need to grep or search manually
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```typescript
|
||||
// The types ARE the documentation
|
||||
// Frontend developers see exactly what the API returns
|
||||
```
|
||||
|
||||
## Current Shared Types
|
||||
|
||||
### Authentication (`auth.types.ts`)
|
||||
- `AuthUser` - Authenticated user info
|
||||
- `AuthSession` - Session data
|
||||
- `Session` - Full session entity
|
||||
- `Account` - OAuth account entity
|
||||
- `LoginRequest`, `LoginResponse`
|
||||
- `OAuthProvider`, `OAuthCallbackParams`
|
||||
|
||||
### Database Entities (`database.types.ts`)
|
||||
- `User` - Full user entity
|
||||
- `Workspace`, `WorkspaceMember`
|
||||
- `Task`, `Event`, `Project`
|
||||
- `ActivityLog`, `MemoryEmbedding`
|
||||
|
||||
### Enums (`enums.ts`)
|
||||
- `TaskStatus`, `TaskPriority`
|
||||
- `ProjectStatus`
|
||||
- `WorkspaceMemberRole`
|
||||
- `ActivityAction`, `EntityType`
|
||||
|
||||
### API Utilities (`index.ts`)
|
||||
- `ApiResponse<T>` - Standard response wrapper
|
||||
- `PaginatedResponse<T>` - Paginated data
|
||||
- `HealthStatus` - Health check format
|
||||
|
||||
---
|
||||
|
||||
**Remember:** If a type is used by both frontend and backend, it belongs in `@mosaic/shared`. If it's only used by one side, keep it local to that application.
|
||||
Reference in New Issue
Block a user