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,82 @@
import { Injectable, Logger } from "@nestjs/common";
import type { PrismaClient } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import { createAuth, type Auth } from "./auth.config";
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly auth: Auth;
constructor(private readonly prisma: PrismaService) {
// PrismaService extends PrismaClient and is compatible with BetterAuth's adapter
// Cast is safe as PrismaService provides all required PrismaClient methods
this.auth = createAuth(this.prisma as unknown as PrismaClient);
}
/**
* Get BetterAuth instance
*/
getAuth() {
return this.auth;
}
/**
* Get user by ID
*/
async getUserById(userId: string) {
return this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
authProviderId: true,
},
});
}
/**
* Get user by email
*/
async getUserByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
select: {
id: true,
email: true,
name: true,
authProviderId: true,
},
});
}
/**
* Verify session token
* Returns session data if valid, null if invalid or expired
*/
async verifySession(token: string): Promise<{ user: any; session: any } | null> {
try {
const session = await this.auth.api.getSession({
headers: {
authorization: `Bearer ${token}`,
},
});
if (!session) {
return null;
}
return {
user: session.user,
session: session.session,
};
} catch (error) {
this.logger.error(
"Session verification failed",
error instanceof Error ? error.message : "Unknown error"
);
return null;
}
}
}