Files
stack/apps/api/src/auth/auth.service.ts
Jason Woltje ef25167c24 fix(#196): fix race condition in job status updates
Implemented optimistic locking with version field and SELECT FOR UPDATE
transactions to prevent data corruption from concurrent job status updates.

Changes:
- Added version field to RunnerJob schema for optimistic locking
- Created migration 20260202_add_runner_job_version_for_concurrency
- Implemented ConcurrentUpdateException for conflict detection
- Updated RunnerJobsService methods with optimistic locking:
  * updateStatus() - with version checking and retry logic
  * updateProgress() - with version checking and retry logic
  * cancel() - with version checking and retry logic
- Updated CoordinatorIntegrationService with SELECT FOR UPDATE:
  * updateJobStatus() - transaction with row locking
  * completeJob() - transaction with row locking
  * failJob() - transaction with row locking
  * updateJobProgress() - optimistic locking
- Added retry mechanism (3 attempts) with exponential backoff
- Added comprehensive concurrency tests (10 tests, all passing)
- Updated existing test mocks to support updateMany

Test Results:
- All 10 concurrency tests passing ✓
- Tests cover concurrent status updates, progress updates, completions,
  cancellations, retry logic, and exponential backoff

This fix prevents race conditions that could cause:
- Lost job results (double completion)
- Lost progress updates
- Invalid status transitions
- Data corruption under concurrent access

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 12:51:17 -06:00

95 lines
2.2 KiB
TypeScript

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(): Auth {
return this.auth;
}
/**
* Get user by ID
*/
async getUserById(userId: string): Promise<{
id: string;
email: string;
name: string;
authProviderId: string | null;
} | null> {
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): Promise<{
id: string;
email: string;
name: string;
authProviderId: string | null;
} | null> {
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: Record<string, unknown>; session: Record<string, unknown> } | null> {
try {
const session = await this.auth.api.getSession({
headers: {
authorization: `Bearer ${token}`,
},
});
if (!session) {
return null;
}
return {
user: session.user as Record<string, unknown>,
session: session.session as Record<string, unknown>,
};
} catch (error) {
this.logger.error(
"Session verification failed",
error instanceof Error ? error.message : "Unknown error"
);
return null;
}
}
}