Implement Workstream H: Auth pages with refactored components

Add 10 authentication pages for the Publisher Dashboard:
- H1: /auth/signup - Account creation with passkey/password
- H2: /auth/setup/user - Profile setup with phone validation
- H3: /auth/login - Email entry with routing logic
- H4: /auth/login/passkey - WebAuthn authentication
- H5: /auth/login/password - Password authentication
- H6: /auth/confirm - Email verification polling
- H7: /auth/trust-device - Device trust prompt
- H8: /auth/verify - Email verification callback
- H9: /auth/forgot-password - Password reset request
- H10: /auth/reset-password - New password form

New reusable components:
- LoadingButton: Button with Loader2 spinner and loading state
- ErrorAlert: Accessible error display with ARIA live region
- PasswordFormField: Composite field with label, input, strength meter
- PasswordInput: Improved with bind:value and cn() class merging

New utilities:
- validation.ts: Email, phone validation, email masking, error parsing
- auth.svelte.ts: Login flow state store for SPA mode guards

Backend updates:
- Implement me.get, me.setupProfile, me.getDeviceInfo, me.trustDevice

Dependencies added:
- @simplewebauthn/browser, libphonenumber-js, ua-parser-js
- zxcvbn, svelte-sonner, shadcn alert component

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 16:32:35 +08:00
parent 93851afe38
commit 073db98a91
30 changed files with 2138 additions and 228 deletions

View File

@@ -105,14 +105,47 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication
});
// Me procedures
const meGet = os.me.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
const meGet = os.me.get.use(authMiddleware).handler(async ({ context }) => {
const user = await context.db
.selectFrom("users")
.where("id", "=", context.user.id)
.select([
"id",
"email",
"display_name",
"full_name",
"phone_number",
"avatar_url",
"email_verified_at",
"is_superuser",
])
.executeTakeFirstOrThrow();
return {
id: user.id,
email: user.email,
displayName: user.display_name,
fullName: user.full_name,
phoneNumber: user.phone_number,
avatarUrl: user.avatar_url,
emailVerified: !!user.email_verified_at,
needsSetup: !user.display_name,
isSuperuser: user.is_superuser,
};
});
const setupProfile = os.me.setupProfile
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
.handler(async ({ input, context }) => {
await context.db
.updateTable("users")
.set({
display_name: input.displayName,
full_name: input.fullName ?? null,
phone_number: input.phoneNumber ?? null,
})
.where("id", "=", context.user.id)
.execute();
});
const updateProfile = os.me.updateProfile
@@ -206,13 +239,44 @@ const revokeAllSessions = os.me.revokeAllSessions
const getDeviceInfo = os.me.getDeviceInfo
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
.handler(async ({ context }) => {
const session = await context.db
.selectFrom("sessions")
.where("id", "=", context.session.id)
.select([
"ip_address",
"city",
"region",
"country",
"user_agent",
])
.executeTakeFirstOrThrow();
return {
id: 0,
name: "Unknown Device",
ip: session.ip_address ?? "Unknown",
city: session.city,
region: session.region,
country: session.country,
lastUsedAt: new Date(),
isTrusted: context.session.trustedMode,
};
});
const trustDevice = os.me.trustDevice.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const trustDevice = os.me.trustDevice
.use(authMiddleware)
.handler(async ({ input, context }) => {
// Note: Sessions table doesn't have a device_name field
// The name parameter is accepted by the contract but not stored
await context.db
.updateTable("sessions")
.set({
trusted_mode: true,
})
.where("id", "=", context.session.id)
.execute();
});
const listTrustedDevices = os.me.listTrustedDevices
.use(authMiddleware)