fix(auth): use explicit public. schema for all login/register queries to prevent search_path contamination
This commit is contained in:
parent
c2ba432341
commit
00357d855d
|
|
@ -54,7 +54,29 @@ export class AuthService {
|
|||
refreshToken: string;
|
||||
user: { id: string; email?: string; phone?: string; name: string; roles: string[]; tenantId: string };
|
||||
}> {
|
||||
const user = await this.userRepository.findByIdentifier(identifier);
|
||||
// Always query public.users explicitly — avoids search_path contamination
|
||||
const isPhone = /^[+\d][\d\s\-().]{6,}$/.test(identifier);
|
||||
const rows = isPhone
|
||||
? await this.dataSource.query(
|
||||
`SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [identifier],
|
||||
)
|
||||
: await this.dataSource.query(
|
||||
`SELECT * FROM public.users WHERE email = $1 AND is_active = true LIMIT 1`, [identifier],
|
||||
);
|
||||
if (!rows.length) {
|
||||
// fallback: if phone not found try email and vice versa
|
||||
const fallbackRows = isPhone
|
||||
? await this.dataSource.query(
|
||||
`SELECT * FROM public.users WHERE email = $1 AND is_active = true LIMIT 1`, [identifier],
|
||||
)
|
||||
: await this.dataSource.query(
|
||||
`SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [identifier],
|
||||
);
|
||||
if (!fallbackRows.length) throw new UnauthorizedException('Invalid credentials');
|
||||
rows.push(...fallbackRows);
|
||||
}
|
||||
const rawUser = rows[0];
|
||||
const user = Object.assign(new User(), rawUser) as User;
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
|
@ -103,14 +125,18 @@ export class AuthService {
|
|||
throw new BadRequestException('Email or phone is required');
|
||||
}
|
||||
|
||||
// Check uniqueness
|
||||
// Check uniqueness — always query public.users explicitly to avoid search_path issues
|
||||
if (email) {
|
||||
const existing = await this.userRepository.findByEmail(email);
|
||||
if (existing) throw new ConflictException('Email already registered');
|
||||
const rows = await this.dataSource.query(
|
||||
`SELECT id FROM public.users WHERE email = $1 LIMIT 1`, [email],
|
||||
);
|
||||
if (rows.length > 0) throw new ConflictException('Email already registered');
|
||||
}
|
||||
if (phone) {
|
||||
const existing = await this.userRepository.findByPhone(phone);
|
||||
if (existing) throw new ConflictException('Phone number already registered');
|
||||
const rows = await this.dataSource.query(
|
||||
`SELECT id FROM public.users WHERE phone = $1 LIMIT 1`, [phone],
|
||||
);
|
||||
if (rows.length > 0) throw new ConflictException('Phone number already registered');
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
|
@ -200,6 +226,35 @@ export class AuthService {
|
|||
const now = new Date();
|
||||
const schemaName = `it0_t_${slug}`;
|
||||
|
||||
// 3+4. Insert into both public.users (auth lookup) AND tenant schema (management) in one QR
|
||||
const qr = this.dataSource.createQueryRunner();
|
||||
await qr.connect();
|
||||
try {
|
||||
await qr.startTransaction();
|
||||
|
||||
// a. Insert into public.users — explicit schema qualifier, no search_path dependency
|
||||
await qr.query(
|
||||
`INSERT INTO public.users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
|
||||
);
|
||||
|
||||
// b. Insert into tenant schema for tenant-context management
|
||||
await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
|
||||
await qr.query(
|
||||
`INSERT INTO users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
|
||||
);
|
||||
|
||||
await qr.commitTransaction();
|
||||
} catch (err) {
|
||||
await qr.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await qr.release();
|
||||
}
|
||||
|
||||
const user = new User();
|
||||
user.id = userId;
|
||||
user.tenantId = slug;
|
||||
|
|
@ -209,28 +264,6 @@ export class AuthService {
|
|||
user.name = name;
|
||||
user.roles = [RoleType.ADMIN];
|
||||
user.isActive = true;
|
||||
user.createdAt = now;
|
||||
user.updatedAt = now;
|
||||
await this.userRepository.save(user); // writes to public.users (auth lookup index)
|
||||
|
||||
// 4. Also insert into the tenant schema for tenant-context management
|
||||
const qr = this.dataSource.createQueryRunner();
|
||||
await qr.connect();
|
||||
try {
|
||||
await qr.startTransaction();
|
||||
await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
|
||||
await qr.query(
|
||||
`INSERT INTO users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
|
||||
);
|
||||
await qr.commitTransaction();
|
||||
} catch (err) {
|
||||
await qr.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await qr.release();
|
||||
}
|
||||
|
||||
const tokens = this.generateTokens(user);
|
||||
return {
|
||||
|
|
@ -445,10 +478,13 @@ export class AuthService {
|
|||
throw new UnauthorizedException('Invalid token type');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(payload.sub);
|
||||
if (!user || !user.isActive) {
|
||||
const rows = await this.dataSource.query(
|
||||
`SELECT * FROM public.users WHERE id = $1 AND is_active = true LIMIT 1`, [payload.sub],
|
||||
);
|
||||
if (!rows.length) {
|
||||
throw new UnauthorizedException('User not found or inactive');
|
||||
}
|
||||
const user = Object.assign(new User(), rows[0]) as User;
|
||||
|
||||
return this.generateTokens(user);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue