Files
edr-platform/apps/edr-freight-api/src/modules/otp/otp.repository.ts
2026-07-20 12:10:49 +00:00

188 lines
4.7 KiB
TypeScript

// otp.repository.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { FindOptionsWhere, Repository } from "typeorm";
import { OtpVerification } from "./otp.entity";
type Target = { phone?: string; email?: string };
@Injectable()
export class OtpRepository {
constructor(
@InjectRepository(
OtpVerification
)
private readonly repository: Repository<OtpVerification>
) {}
// ---------------------------------------------------------------------------
// Find By Phone
// ---------------------------------------------------------------------------
async findByPhone(
phone: string
) {
return this.repository.findOne({
where: {
phone,
},
});
}
// ---------------------------------------------------------------------------
// Find By Email
// ---------------------------------------------------------------------------
async findByEmail(
email: string
) {
return this.repository.findOne({
where: {
email,
},
});
}
// ---------------------------------------------------------------------------
// Find By Target (any named channel)
// ---------------------------------------------------------------------------
/**
* OR across every channel the target names. A code sent to both phone and
* email lives in ONE row carrying both values, so a verify that quotes either
* one resolves the same row — that is what makes "sent to both, verify with
* either" work.
*/
private whereForTarget(
target: Target
): FindOptionsWhere<OtpVerification>[] {
const where: FindOptionsWhere<OtpVerification>[] =
[];
if (target.email)
where.push({
email: target.email,
});
if (target.phone)
where.push({
phone: target.phone,
});
return where;
}
async findAllByTarget(
target: Target
) {
const where =
this.whereForTarget(target);
if (!where.length) return [];
// Newest first: a target that somehow overlaps two legacy single-channel
// rows should resolve to the most recently issued code, not an arbitrary one.
return this.repository.find({
where,
order: { updatedAt: "DESC" },
});
}
async findByTarget(
target: Target
) {
const [
newest,
] = await this.findAllByTarget(
target
);
return newest ?? null;
}
// ---------------------------------------------------------------------------
// Replace OTP (upsert across every channel the target names)
// ---------------------------------------------------------------------------
/**
* Drop every row this target overlaps and write a single fresh one holding
* all its channels.
*
* `phone` and `email` are each UNIQUE, so a dual-channel send can collide with
* up to two pre-existing single-channel rows (say an old signup code on the
* phone and a reset code on the email). Merging into one row instead of
* updating in place is what keeps that from raising a unique violation, and it
* preserves the single-use guarantee: consuming the code deletes one row and
* kills every channel it was sent to at once.
*
* "Last code sent wins" was already the behaviour between any two flows
* sharing this table — this only widens it from one channel to all of them.
*/
async replaceOtp(
target: Target,
otp: string
): Promise<{
record: OtpVerification;
rotated: boolean;
}> {
const existing =
await this.findAllByTarget(
target
);
if (existing.length) {
await this.repository.remove(
existing
);
}
const record =
await this.repository.save(
this.repository.create({
phone: target.phone,
email: target.email,
otp,
verified: false,
})
);
return {
record,
rotated: existing.length > 0,
};
}
// ---------------------------------------------------------------------------
// Mark Verified
// ---------------------------------------------------------------------------
async markVerified(
otpVerification: OtpVerification
) {
otpVerification.verified =
true;
return this.repository.save(
otpVerification
);
}
// ---------------------------------------------------------------------------
// Delete OTP (single-use consume)
// ---------------------------------------------------------------------------
// Hard delete so the unique `phone`/`email` rows are freed and a fresh code can
// be requested for the same target on the next action.
async deleteOtp(
otpVerification: OtpVerification
) {
return this.repository.remove(
otpVerification
);
}
}