mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
188 lines
4.7 KiB
TypeScript
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
|
|
);
|
|
}
|
|
}
|