feat(auth): migrate anonymous consent on signUp when cc_consent cookie present
Adds ConsentFactoryProtocol / ConsentGrantMeta / ConsentProtocol to core-shared/di/bind-protocols so feature binders can wire per-user consent without a hard dep on the optional @repo/core-consent package. BindContext gains an optional consentFactory? field following the same pattern as bus?, auditLog?, etc. signUpUseCase gains a 4th optional dep (consentFactory). When present and the input includes a cookieHeader containing cc_consent=<categories>, the use case calls consent.grant for each category with method:"signup-migration" and returns a clearCookie payload (Max-Age:0) so the anonymous cookie is cleared on the HTTP response. Tests use RecordingConsent from @repo/core-testing to assert migration call shape and cookie-clear; no-cookie and no-factory branches are also covered. All coverage bands hold at 100% for use-cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { EventBusProtocol } from "@repo/core-shared/di";
|
||||
import type {
|
||||
EventBusProtocol,
|
||||
ConsentFactoryProtocol,
|
||||
} from "@repo/core-shared/di";
|
||||
import { userSignedUpEvent } from "../../events/user-signed-up.event";
|
||||
import { AuthenticationError } from "../../entities/errors/auth";
|
||||
import { cookieSchema } from "../../entities/models/cookie";
|
||||
@@ -8,12 +11,32 @@ import { sessionSchema } from "../../entities/models/session";
|
||||
import type { IUsersRepository } from "../repositories/users.repository.interface";
|
||||
import type { IAuthenticationService } from "../services/authentication.service.interface";
|
||||
|
||||
// Cookie name written by the anonymous consent banner (mirrors CONSENT_COOKIE_NAME in @repo/core-consent).
|
||||
const ANONYMOUS_CONSENT_COOKIE = "cc_consent";
|
||||
|
||||
function extractConsentFromCookieHeader(cookieHeader: string): string[] | null {
|
||||
for (const part of cookieHeader.split(";")) {
|
||||
const eqIdx = part.indexOf("=");
|
||||
if (eqIdx === -1) continue;
|
||||
const name = part.slice(0, eqIdx).trim();
|
||||
if (name !== ANONYMOUS_CONSENT_COOKIE) continue;
|
||||
const value = part.slice(eqIdx + 1).trim();
|
||||
const cats = value
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean);
|
||||
return cats.length > 0 ? cats : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────────────
|
||||
export const signUpInputSchema = z
|
||||
.object({
|
||||
username: z.string().min(3).max(31),
|
||||
password: z.string().min(6).max(255),
|
||||
confirmPassword: z.string().min(6).max(255),
|
||||
cookieHeader: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((d) => d.password === d.confirmPassword, {
|
||||
@@ -26,6 +49,7 @@ export type SignUpInput = z.infer<typeof signUpInputSchema>;
|
||||
export const signUpOutputSchema = z.object({
|
||||
session: sessionSchema,
|
||||
cookie: cookieSchema,
|
||||
clearCookie: cookieSchema.optional(),
|
||||
});
|
||||
export type SignUpOutput = z.infer<typeof signUpOutputSchema>;
|
||||
|
||||
@@ -37,6 +61,7 @@ export const signUpUseCase =
|
||||
usersRepository: IUsersRepository,
|
||||
authenticationService: IAuthenticationService,
|
||||
bus: EventBusProtocol | undefined,
|
||||
consentFactory: ConsentFactoryProtocol | undefined,
|
||||
) =>
|
||||
async (input: SignUpInput): Promise<SignUpOutput> => {
|
||||
const existingUser = await usersRepository.getUserByUsername(
|
||||
@@ -71,5 +96,24 @@ export const signUpUseCase =
|
||||
});
|
||||
}
|
||||
|
||||
return signUpOutputSchema.parse({ session, cookie });
|
||||
// Migrate anonymous consent when both a cookie header and a consent factory
|
||||
// are present. consentFactory is optional: absent when core-consent is not wired.
|
||||
const cookieState = input.cookieHeader
|
||||
? extractConsentFromCookieHeader(input.cookieHeader)
|
||||
: null;
|
||||
|
||||
let clearCookie: z.infer<typeof cookieSchema> | undefined;
|
||||
if (cookieState && consentFactory) {
|
||||
const consent = await consentFactory(newUser.id);
|
||||
for (const category of cookieState) {
|
||||
await consent.grant(category, { method: "signup-migration" });
|
||||
}
|
||||
clearCookie = {
|
||||
name: ANONYMOUS_CONSENT_COOKIE,
|
||||
value: "",
|
||||
attributes: { maxAge: 0, path: "/" },
|
||||
};
|
||||
}
|
||||
|
||||
return signUpOutputSchema.parse({ session, cookie, clearCookie });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user