levelled up logging, albeit with a bit of verbosity...

This commit is contained in:
user
2026-03-01 04:36:17 +02:00
parent 596dcc78fc
commit 5bf1148a4f
6 changed files with 731 additions and 277 deletions

View File

@@ -3,13 +3,13 @@ import { FlowExecCtx } from "@core/flow.execution.context";
import { UserRepository } from "@domains/user/repository";
import { getRedisInstance, Redis } from "@pkg/keystore";
import { TwofaRepository } from "./repository";
import { logDomainEvent } from "@pkg/logger";
import { auth } from "../auth/config.base";
import type { TwoFaSession } from "./data";
import { User } from "@domains/user/data";
import { settings } from "@core/settings";
import { type Err } from "@pkg/result";
import { twofaErrors } from "./errors";
import { logger } from "@pkg/logger";
import { db } from "@pkg/db";
export class TwofaController {
@@ -33,7 +33,13 @@ export class TwofaController {
isUserBanned(fctx: FlowExecCtx, userId: string) {
return this.userRepo.isUserBanned(fctx, userId).orElse((error) => {
logger.error("Error checking user ban status:", error);
logDomainEvent({
level: "error",
event: "security.twofa.user_ban_check.failed",
fctx,
error,
meta: { userId },
});
return okAsync(false);
});
}
@@ -58,19 +64,34 @@ export class TwofaController {
code: string,
headers: Headers,
) {
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.verify_and_enable.started",
fctx,
meta: { userId: user.id },
});
return this.is2faEnabled(fctx, user.id)
.andThen((enabled) => {
if (enabled) {
logDomainEvent({
level: "warn",
event: "security.twofa.verify_and_enable.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "ALREADY_ENABLED",
message: "2FA already enabled",
},
meta: { userId: user.id },
});
return errAsync(twofaErrors.alreadyEnabled(fctx));
}
return okAsync(undefined);
})
.andThen(() => {
logger.info(`Verifying 2fa for ${user.id} : ${code}`, {
flowId: fctx.flowId,
});
return this.twofaRepo.verifyAndEnable2FA(fctx, user.id, code);
})
.andThen(() =>
this.twofaRepo.verifyAndEnable2FA(fctx, user.id, code),
)
.andThen((verified) => {
if (verified) {
return ResultAsync.combine([
@@ -82,8 +103,27 @@ export class TwofaController {
fctx,
user.id,
),
]).map(() => true);
]).map(() => {
logDomainEvent({
event: "security.twofa.verify_and_enable.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId: user.id },
});
return true;
});
}
logDomainEvent({
level: "warn",
event: "security.twofa.verify_and_enable.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "INVALID_CODE",
message: "2FA code verification failed",
},
meta: { userId: user.id },
});
return okAsync(verified);
});
}
@@ -169,7 +209,12 @@ export class TwofaController {
)
.map(() => undefined)
.orElse((error) => {
logger.error("Error marking initial 2FA as complete:", error);
logDomainEvent({
level: "error",
event: "security.twofa.mark_initial_verification.failed",
fctx: { flowId: crypto.randomUUID() },
error,
});
return okAsync(undefined);
});
}

View File

@@ -11,7 +11,7 @@ import { settings } from "@core/settings";
import type { Err } from "@pkg/result";
import { twofaErrors } from "./errors";
import { Redis } from "@pkg/keystore";
import { logger } from "@pkg/logger";
import { logDomainEvent, logger } from "@pkg/logger";
import { nanoid } from "nanoid";
type TwoFaSetup = {
@@ -53,25 +53,49 @@ export class TwofaRepository {
userId: string,
returnUndefined?: boolean,
): ResultAsync<TwoFactor | undefined, Err> {
logger.info("Getting user 2FA info", { ...fctx, userId });
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.get_info.started",
fctx,
meta: { userId },
});
return ResultAsync.fromPromise(
this.db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, userId),
}),
() => twofaErrors.dbError(fctx, "Failed to query 2FA info"),
(error) => {
logDomainEvent({
level: "error",
event: "security.twofa.get_info.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId },
});
return twofaErrors.dbError(fctx, "Failed to query 2FA info");
},
).andThen((found) => {
if (!found) {
logger.debug("2FA info not found for user", {
...fctx,
userId,
logDomainEvent({
level: "warn",
event: "security.twofa.get_info.failed",
fctx,
durationMs: Date.now() - startedAt,
error: { code: "NOT_FOUND", message: "2FA info not found" },
meta: { userId },
});
if (returnUndefined) {
return okAsync(undefined);
}
return errAsync(twofaErrors.notFound(fctx));
}
logger.info("2FA info retrieved successfully", { ...fctx, userId });
logDomainEvent({
event: "security.twofa.get_info.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId },
});
return okAsync(found as TwoFactor);
});
}
@@ -105,7 +129,12 @@ export class TwofaRepository {
userId: string,
secret: string,
): ResultAsync<string, Err> {
logger.info("Starting 2FA setup", { ...fctx, userId });
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.setup.started",
fctx,
meta: { userId },
});
return ResultAsync.fromSafePromise(
(async () => {
@@ -124,16 +153,25 @@ export class TwofaRepository {
this.EXPIRY_TIME,
JSON.stringify(payload),
);
logger.info("Created temp 2FA session", {
...fctx,
userId,
expiresIn: this.EXPIRY_TIME,
logDomainEvent({
event: "security.twofa.setup.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId, expiresInSec: this.EXPIRY_TIME },
});
return secret;
})(),
).mapErr(() =>
twofaErrors.dbError(fctx, "Setting to data store failed"),
);
).mapErr((error) => {
logDomainEvent({
level: "error",
event: "security.twofa.setup.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId },
});
return twofaErrors.dbError(fctx, "Setting to data store failed");
});
}
verifyAndEnable2FA(
@@ -141,7 +179,12 @@ export class TwofaRepository {
userId: string,
code: string,
): ResultAsync<boolean, Err> {
logger.info("Verifying and enabling 2FA", { ...fctx, userId });
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.verify_enable.started",
fctx,
meta: { userId },
});
return ResultAsync.fromPromise(
this.store.get(this.getKey(userId)),
@@ -149,9 +192,16 @@ export class TwofaRepository {
)
.andThen((payload) => {
if (!payload) {
logger.error("Setup session not found", {
...fctx,
userId,
logDomainEvent({
level: "warn",
event: "security.twofa.verify_enable.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "SETUP_NOT_FOUND",
message: "2FA setup session not found",
},
meta: { userId },
});
return errAsync(twofaErrors.setupNotFound(fctx));
}
@@ -161,10 +211,16 @@ export class TwofaRepository {
const key = this.getKey(userId);
if (payloadObj.tries >= this.MAX_SETUP_ATTEMPTS) {
logger.warn("Max setup attempts reached", {
...fctx,
userId,
tries: payloadObj.tries,
logDomainEvent({
level: "warn",
event: "security.twofa.verify_enable.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "MAX_ATTEMPTS_REACHED",
message: "Max setup attempts reached",
},
meta: { userId, attempts: payloadObj.tries },
});
return ResultAsync.fromPromise(this.store.del(key), () =>
twofaErrors.dbError(
@@ -180,11 +236,20 @@ export class TwofaRepository {
!this.checkTotp(payloadObj.secret, code) ||
code === payloadObj.lastUsedCode
) {
logger.warn("Invalid 2FA code during setup", {
...fctx,
userId,
tries: payloadObj.tries + 1,
codeReused: code === payloadObj.lastUsedCode,
logDomainEvent({
level: "warn",
event: "security.twofa.verify_enable.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "INVALID_CODE",
message: "Invalid or replayed setup code",
},
meta: {
userId,
attempts: payloadObj.tries + 1,
codeReused: code === payloadObj.lastUsedCode,
},
});
return ResultAsync.fromPromise(
this.store.setex(
@@ -232,9 +297,11 @@ export class TwofaRepository {
),
)
.map(() => {
logger.info("2FA enabled successfully", {
...fctx,
userId,
logDomainEvent({
event: "security.twofa.verify_enable.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId },
});
return true;
});
@@ -368,10 +435,11 @@ export class TwofaRepository {
userAgent?: string;
},
): ResultAsync<TwoFaSession, Err> {
logger.info("Creating 2FA verification session", {
...fctx,
userId: params.userId,
sessionId: params.sessionId,
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.create_session.started",
fctx,
meta: { userId: params.userId, sessionId: params.sessionId },
});
return ResultAsync.fromSafePromise(
@@ -402,12 +470,29 @@ export class TwofaRepository {
userAgent: params.userAgent,
})
.returning(),
() => twofaErrors.dbError(fctx, "Failed to create 2FA session"),
(error) => {
logDomainEvent({
level: "error",
event: "security.twofa.create_session.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId: params.userId },
});
return twofaErrors.dbError(
fctx,
"Failed to create 2FA session",
);
},
).map(([session]) => {
logger.info("2FA verification session created", {
...fctx,
sessionId: session.id,
userId: params.userId,
logDomainEvent({
event: "security.twofa.create_session.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: {
twofaSessionId: session.id,
userId: params.userId,
},
});
return session as TwoFaSession;
}),
@@ -418,7 +503,12 @@ export class TwofaRepository {
fctx: FlowExecCtx,
token: string,
): ResultAsync<TwoFaSession | null, Err> {
logger.debug("Getting 2FA session by token", { ...fctx });
const startedAt = Date.now();
logDomainEvent({
level: "debug",
event: "security.twofa.get_session.started",
fctx,
});
return ResultAsync.fromPromise(
this.db
@@ -431,15 +521,36 @@ export class TwofaRepository {
),
)
.limit(1),
() => twofaErrors.dbError(fctx, "Failed to query 2FA session"),
(error) => {
logDomainEvent({
level: "error",
event: "security.twofa.get_session.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
});
return twofaErrors.dbError(fctx, "Failed to query 2FA session");
},
).map((result) => {
if (!result.length) {
logger.warn("2FA session not found or expired", { ...fctx });
logDomainEvent({
level: "warn",
event: "security.twofa.get_session.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "SESSION_NOT_FOUND",
message: "2FA session not found or expired",
},
});
return null;
}
logger.debug("2FA session found", {
...fctx,
sessionId: result[0].id,
logDomainEvent({
level: "debug",
event: "security.twofa.get_session.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { twofaSessionId: result[0].id },
});
return result[0] as TwoFaSession;
});
@@ -547,17 +658,37 @@ export class TwofaRepository {
}
cleanupExpiredSessions(fctx: FlowExecCtx): ResultAsync<number, Err> {
logger.info("Cleaning up expired 2FA sessions", { ...fctx });
const startedAt = Date.now();
logDomainEvent({
event: "security.twofa.cleanup_expired.started",
fctx,
});
return ResultAsync.fromPromise(
this.db
.delete(twofaSessions)
.where(lt(twofaSessions.expiresAt, new Date())),
() =>
twofaErrors.dbError(fctx, "Failed to cleanup expired sessions"),
(error) => {
logDomainEvent({
level: "error",
event: "security.twofa.cleanup_expired.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
});
return twofaErrors.dbError(
fctx,
"Failed to cleanup expired sessions",
);
},
).map((result) => {
const count = result.length || 0;
logger.info("Expired sessions cleaned up", { ...fctx, count });
logDomainEvent({
event: "security.twofa.cleanup_expired.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { count },
});
return count;
});
}