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:
Jason Woltje
2026-01-28 17:26:34 -06:00
parent 139a16648d
commit 6a038d093b
22 changed files with 2616 additions and 7 deletions

View File

@@ -0,0 +1,66 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import type { AuthUser } from "@mosaic/shared";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
describe("AuthController", () => {
let controller: AuthController;
let authService: AuthService;
const mockAuthService = {
getAuth: vi.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{
provide: AuthService,
useValue: mockAuthService,
},
],
}).compile();
controller = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);
vi.clearAllMocks();
});
describe("handleAuth", () => {
it("should call BetterAuth handler", async () => {
const mockHandler = vi.fn().mockResolvedValue({ status: 200 });
mockAuthService.getAuth.mockReturnValue({ handler: mockHandler });
const mockRequest = {
method: "GET",
url: "/auth/session",
};
await controller.handleAuth(mockRequest);
expect(mockAuthService.getAuth).toHaveBeenCalled();
expect(mockHandler).toHaveBeenCalledWith(mockRequest);
});
});
describe("getProfile", () => {
it("should return user profile", () => {
const mockUser: AuthUser = {
id: "user-123",
email: "test@example.com",
name: "Test User",
};
const result = controller.getProfile(mockUser);
expect(result).toEqual({
id: mockUser.id,
email: mockUser.email,
name: mockUser.name,
});
});
});
});