Skip to content

Commit 682b6a4

Browse files
authored
feat: add repository debug logging (#22)
* feat: add repository debug logging * docs: clarify repository log suppression * chore
1 parent a9af92e commit 682b6a4

File tree

17 files changed

+4012
-2536
lines changed

17 files changed

+4012
-2536
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Optional:
2424
```bash
2525
NEXTAUTH_URL=http://localhost:3000
2626
NEXTAUTH_SECRET=...
27+
LOG_LEVEL=<trace|debug|info|warn|error|fatal|silent>
2728
RATE_LIMIT_PROVIDER=<disabled|memory|redis|upstash>
2829
RATE_LIMIT_REDIS_URL=redis://...
2930
UPSTASH_REDIS_REST_URL=...
@@ -99,6 +100,8 @@ pnpm dev
99100

100101
`next dev`, `next build`, and `next start` now validate the required application environment before the app fully boots. Missing or invalid values fail fast with one aggregated error message.
101102

103+
`LOG_LEVEL` controls server log verbosity for the shared Pino logger. It defaults to `info`. Set `LOG_LEVEL=debug` to emit repository debug logs, or `LOG_LEVEL=silent` to suppress them.
104+
102105
When you run `npm start` or `pnpm start`, a `prestart` hook now verifies that local PostgreSQL is reachable at `localhost:54329` when `DATABASE_URL` targets the local dev database. If it is not running, start it with:
103106

104107
```bash

lib/auth/onboarding.ts

Lines changed: 167 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sql from "@/lib/db";
22
import { AuthFlowError } from "@/lib/auth/errors";
33
import { INTERNAL_AUTH_PROVIDER, resolveAppSessionIdentity } from "@/lib/auth/identity";
4+
import { logRepositoryOperation } from "@/lib/repository-logging";
45
import {
56
parseCountryCode,
67
parseFavoriteGenres,
@@ -44,6 +45,8 @@ type CompleteSignupInput = {
4445
userId: string;
4546
};
4647

48+
const REPOSITORY_MODULE = "auth.onboarding";
49+
4750
function mapCompletedUser(row: SignupUserRow): AuthUser {
4851
const sessionIdentity = resolveAppSessionIdentity(row);
4952

@@ -68,152 +71,169 @@ function mapCompletedUser(row: SignupUserRow): AuthUser {
6871
export async function completeSignup(
6972
input: CompleteSignupInput,
7073
): Promise<AuthUser> {
71-
const nickname = parseNickname(input.nickname);
72-
const gender = parseUserGender(input.gender);
73-
const countryCode = parseCountryCode(input.countryCode);
74-
const favoriteGenres = parseFavoriteGenres(input.favoriteGenres);
75-
const invitationCodeHash = hashInvitationCode(input.invitationCode);
76-
77-
try {
78-
return await sql.begin(async (tx) => {
79-
const query = tx as unknown as typeof sql;
80-
81-
const [user] = await query<SignupUserRow[]>`
82-
select
83-
id::text as id,
84-
provider,
85-
provider_user_id as "providerUserId",
86-
email::text as email,
87-
name,
88-
image_url as "imageUrl",
89-
nickname,
90-
gender,
91-
country_code as "countryCode",
92-
favorite_genres as "favoriteGenres",
93-
signup_completed_at as "signupCompletedAt"
94-
from bookapp.users
95-
where id = ${input.userId}::uuid
96-
for update
97-
`;
98-
99-
if (!user) {
100-
throw new AuthFlowError("UNAUTHORIZED", "Sign in to continue.");
101-
}
102-
103-
if (user.provider === INTERNAL_AUTH_PROVIDER) {
104-
throw new AuthFlowError(
105-
"FORBIDDEN",
106-
"Internal admins cannot complete reader signup.",
107-
);
108-
}
109-
110-
if (user.signupCompletedAt) {
111-
throw new AuthFlowError("CONFLICT", "Signup is already complete.");
112-
}
113-
114-
const [invitationCode] = await query<InvitationCodeLookupRow[]>`
115-
select
116-
invitation_codes.id::text as id,
117-
invitation_codes.purpose,
118-
invitation_codes.is_active as "isActive",
119-
invitation_codes.expires_at as "expiresAt",
120-
invitation_codes.max_uses as "maxUses"
121-
from bookapp.invitation_codes
122-
where invitation_codes.code_hash = ${invitationCodeHash}
123-
and invitation_codes.purpose = 'BETA_SIGNUP'
124-
for update
125-
`;
126-
127-
if (!invitationCode) {
128-
throw new AuthFlowError(
129-
"VALIDATION",
130-
"Enter a valid beta invitation code.",
131-
);
132-
}
133-
134-
const [redemptionCountResult] = await query<{ redemptionCount: number }[]>`
135-
select count(*)::int as "redemptionCount"
136-
from bookapp.invitation_code_redemptions
137-
where code_id = ${invitationCode.id}::uuid
138-
`;
139-
140-
const invitationCodeStatus = resolveInvitationCodeStatus({
141-
...invitationCode,
142-
redemptionCount: redemptionCountResult?.redemptionCount ?? 0,
143-
});
144-
switch (invitationCodeStatus) {
145-
case "INACTIVE":
146-
throw new AuthFlowError(
147-
"FORBIDDEN",
148-
"This invitation code is inactive.",
149-
);
150-
case "EXPIRED":
151-
throw new AuthFlowError(
152-
"FORBIDDEN",
153-
"This invitation code has expired.",
154-
);
155-
case "EXHAUSTED":
156-
throw new AuthFlowError(
157-
"FORBIDDEN",
158-
"This invitation code has no uses remaining.",
159-
);
160-
default:
161-
break;
74+
return logRepositoryOperation(
75+
{
76+
context: {
77+
countryCode: input.countryCode,
78+
favoriteGenreCount: input.favoriteGenres.length,
79+
gender: input.gender,
80+
userId: input.userId,
81+
},
82+
module: REPOSITORY_MODULE,
83+
operation: "completeSignup",
84+
transactional: true,
85+
},
86+
async () => {
87+
const nickname = parseNickname(input.nickname);
88+
const gender = parseUserGender(input.gender);
89+
const countryCode = parseCountryCode(input.countryCode);
90+
const favoriteGenres = parseFavoriteGenres(input.favoriteGenres);
91+
const invitationCodeHash = hashInvitationCode(input.invitationCode);
92+
93+
try {
94+
return await sql.begin(async (tx) => {
95+
const query = tx as unknown as typeof sql;
96+
97+
const [user] = await query<SignupUserRow[]>`
98+
select
99+
id::text as id,
100+
provider,
101+
provider_user_id as "providerUserId",
102+
email::text as email,
103+
name,
104+
image_url as "imageUrl",
105+
nickname,
106+
gender,
107+
country_code as "countryCode",
108+
favorite_genres as "favoriteGenres",
109+
signup_completed_at as "signupCompletedAt"
110+
from bookapp.users
111+
where id = ${input.userId}::uuid
112+
for update
113+
`;
114+
115+
if (!user) {
116+
throw new AuthFlowError("UNAUTHORIZED", "Sign in to continue.");
117+
}
118+
119+
if (user.provider === INTERNAL_AUTH_PROVIDER) {
120+
throw new AuthFlowError(
121+
"FORBIDDEN",
122+
"Internal admins cannot complete reader signup.",
123+
);
124+
}
125+
126+
if (user.signupCompletedAt) {
127+
throw new AuthFlowError("CONFLICT", "Signup is already complete.");
128+
}
129+
130+
const [invitationCode] = await query<InvitationCodeLookupRow[]>`
131+
select
132+
invitation_codes.id::text as id,
133+
invitation_codes.purpose,
134+
invitation_codes.is_active as "isActive",
135+
invitation_codes.expires_at as "expiresAt",
136+
invitation_codes.max_uses as "maxUses"
137+
from bookapp.invitation_codes
138+
where invitation_codes.code_hash = ${invitationCodeHash}
139+
and invitation_codes.purpose = 'BETA_SIGNUP'
140+
for update
141+
`;
142+
143+
if (!invitationCode) {
144+
throw new AuthFlowError(
145+
"VALIDATION",
146+
"Enter a valid beta invitation code.",
147+
);
148+
}
149+
150+
const [redemptionCountResult] = await query<
151+
{ redemptionCount: number }[]
152+
>`
153+
select count(*)::int as "redemptionCount"
154+
from bookapp.invitation_code_redemptions
155+
where code_id = ${invitationCode.id}::uuid
156+
`;
157+
158+
const invitationCodeStatus = resolveInvitationCodeStatus({
159+
...invitationCode,
160+
redemptionCount: redemptionCountResult?.redemptionCount ?? 0,
161+
});
162+
switch (invitationCodeStatus) {
163+
case "INACTIVE":
164+
throw new AuthFlowError(
165+
"FORBIDDEN",
166+
"This invitation code is inactive.",
167+
);
168+
case "EXPIRED":
169+
throw new AuthFlowError(
170+
"FORBIDDEN",
171+
"This invitation code has expired.",
172+
);
173+
case "EXHAUSTED":
174+
throw new AuthFlowError(
175+
"FORBIDDEN",
176+
"This invitation code has no uses remaining.",
177+
);
178+
default:
179+
break;
180+
}
181+
182+
const [updatedUser] = await query<SignupUserRow[]>`
183+
update bookapp.users
184+
set
185+
nickname = ${nickname},
186+
gender = ${gender},
187+
country_code = ${countryCode},
188+
favorite_genres = ${sql.array([...favoriteGenres])},
189+
signup_completed_at = now(),
190+
updated_at = now()
191+
where id = ${input.userId}::uuid
192+
returning
193+
id::text as id,
194+
provider,
195+
provider_user_id as "providerUserId",
196+
email::text as email,
197+
name,
198+
image_url as "imageUrl",
199+
nickname,
200+
gender,
201+
country_code as "countryCode",
202+
favorite_genres as "favoriteGenres",
203+
signup_completed_at as "signupCompletedAt"
204+
`;
205+
206+
await query`
207+
insert into bookapp.invitation_code_redemptions (
208+
code_id,
209+
user_id
210+
)
211+
values (
212+
${invitationCode.id}::uuid,
213+
${input.userId}::uuid
214+
)
215+
`;
216+
217+
if (!updatedUser) {
218+
throw new AuthFlowError("UNAUTHORIZED", "Sign in to continue.");
219+
}
220+
221+
return mapCompletedUser(updatedUser);
222+
});
223+
} catch (error) {
224+
if (
225+
typeof error === "object" &&
226+
error !== null &&
227+
"code" in error &&
228+
error.code === "23505" &&
229+
"constraint_name" in error &&
230+
error.constraint_name === "users_nickname_uniq"
231+
) {
232+
throw new AuthFlowError("CONFLICT", "This nickname is already taken.");
233+
}
234+
235+
throw error;
162236
}
163-
164-
const [updatedUser] = await query<SignupUserRow[]>`
165-
update bookapp.users
166-
set
167-
nickname = ${nickname},
168-
gender = ${gender},
169-
country_code = ${countryCode},
170-
favorite_genres = ${sql.array([...favoriteGenres])},
171-
signup_completed_at = now(),
172-
updated_at = now()
173-
where id = ${input.userId}::uuid
174-
returning
175-
id::text as id,
176-
provider,
177-
provider_user_id as "providerUserId",
178-
email::text as email,
179-
name,
180-
image_url as "imageUrl",
181-
nickname,
182-
gender,
183-
country_code as "countryCode",
184-
favorite_genres as "favoriteGenres",
185-
signup_completed_at as "signupCompletedAt"
186-
`;
187-
188-
await query`
189-
insert into bookapp.invitation_code_redemptions (
190-
code_id,
191-
user_id
192-
)
193-
values (
194-
${invitationCode.id}::uuid,
195-
${input.userId}::uuid
196-
)
197-
`;
198-
199-
if (!updatedUser) {
200-
throw new AuthFlowError("UNAUTHORIZED", "Sign in to continue.");
201-
}
202-
203-
return mapCompletedUser(updatedUser);
204-
});
205-
} catch (error) {
206-
if (
207-
typeof error === "object" &&
208-
error !== null &&
209-
"code" in error &&
210-
error.code === "23505" &&
211-
"constraint_name" in error &&
212-
error.constraint_name === "users_nickname_uniq"
213-
) {
214-
throw new AuthFlowError("CONFLICT", "This nickname is already taken.");
215-
}
216-
217-
throw error;
218-
}
237+
},
238+
);
219239
}

0 commit comments

Comments
 (0)