Implement Workstream I: Account pages with code review fixes

Add account management UI with profile settings, authentication options,
device/passkey management, and session management pages.

Key changes:
- Add account pages: profile, auth, devices, sessions
- Add dialog components: confirm, add-passkey, change-password, rename-passkey
- Return passkeyId from verifyRegistration to fix race condition
- Add hasPassword field to user schema
- Add aria-label to dialog close button for accessibility
- Add avatar URL validation and fix phone input styling
- Add comprehensive test plan documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 18:04:02 +08:00
parent 2655c57b9e
commit 1083cde9b7
32 changed files with 2369 additions and 11 deletions

View File

@@ -49,6 +49,8 @@ function createAPIContext(): APIContext {
origin: TEST_RP.origin,
allowedOrigins: [...TEST_RP.allowedOrigins],
rpName: TEST_RP.rpName,
reqHeaders: new Headers(),
resHeaders: new Headers(),
};
}
@@ -69,7 +71,7 @@ function createAuthenticatedContext(
isSuperuser: false,
},
session: {
id: 1,
id: "1",
trustedMode: false,
createdAt: new Date(),
},

View File

@@ -120,5 +120,5 @@ export async function countOwners(
.where("role", "=", "owner")
.executeTakeFirstOrThrow();
return Number(result.count);
return result.count;
}

View File

@@ -108,7 +108,13 @@ const verifyRegistration = os.auth.webauthn.verifyRegistration
context.allowedOrigins,
context.rpName,
);
await verifyReg(context.db, rpInfo, context.user.id, challengeId, response);
return verifyReg(
context.db,
rpInfo,
context.user.id,
challengeId,
response,
);
});
const createAuthenticationOptions = os.auth.webauthn.createAuthenticationOptions
@@ -161,6 +167,7 @@ const meGet = os.me.get.use(authMiddleware).handler(async ({ context }) => {
"avatar_url",
"email_verified_at",
"is_superuser",
"password_hash",
])
.where("id", "=", context.user.id)
.executeTakeFirstOrThrow();
@@ -175,6 +182,7 @@ const meGet = os.me.get.use(authMiddleware).handler(async ({ context }) => {
emailVerified: user.email_verified_at !== null,
needsSetup: user.display_name === null,
isSuperuser: user.is_superuser,
hasPassword: user.password_hash !== null,
};
});

View File

@@ -157,7 +157,7 @@ export const verifyRegistration = async (
userId: number,
challengeId: number,
response: RegistrationResponseJSON,
): Promise<void> => {
): Promise<{ passkeyId: number }> => {
// Fetch the challenge
const challengeRow = await db
.selectFrom("webauthn_challenges")
@@ -207,7 +207,7 @@ export const verifyRegistration = async (
guidName ?? `Key registered at ${formatPasskeyDate(new Date())}`;
// Store the passkey
await db
const { id: passkeyId } = await db
.insertInto("passkeys")
.values({
user_id: userId,
@@ -222,7 +222,10 @@ export const verifyRegistration = async (
rpid: rpInfo.rpID,
name: passKeyName,
})
.execute();
.returning("id")
.executeTakeFirstOrThrow();
return { passkeyId: Number(passkeyId) };
};
/**