-
Notifications
You must be signed in to change notification settings - Fork 38
Fix/auth #2425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/auth #2425
Changes from all commits
1587e33
b9e0862
a0e210f
e1edb96
19b85f7
420f2eb
cfcad31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { clientEnvs } from '@packrat/env/expo-client'; | ||
| import AsyncStorage from '@react-native-async-storage/async-storage'; | ||
| import { | ||
| GoogleSignin, | ||
|
|
@@ -187,19 +188,27 @@ export function useAuthActions() { | |
| }; | ||
|
|
||
| const forgotPassword = async (email: string) => { | ||
| const { error } = await authClient.requestPasswordReset({ | ||
| email, | ||
| redirectTo: 'packrat://reset-password', | ||
| const res = await fetch(`${clientEnvs.EXPO_PUBLIC_API_URL}/api/password-reset/request`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ email }), | ||
| }); | ||
| if (error) throw new Error(error.message ?? 'Forgot password failed'); | ||
| if (!res.ok) { | ||
| const data = (await res.json()) as { error?: string }; | ||
| throw new Error(data.error ?? 'Forgot password failed'); | ||
| } | ||
| }; | ||
|
|
||
| const resetPassword = async (_email: string, opts: { token: string; newPassword: string }) => { | ||
| const { error } = await authClient.resetPassword({ | ||
| token: opts.token, | ||
| newPassword: opts.newPassword, | ||
| const resetPassword = async (email: string, opts: { token: string; newPassword: string }) => { | ||
| const res = await fetch(`${clientEnvs.EXPO_PUBLIC_API_URL}/api/password-reset/verify`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ email, code: opts.token, newPassword: opts.newPassword }), | ||
| }); | ||
| if (error) throw new Error(error.message ?? 'Reset password failed'); | ||
| if (!res.ok) { | ||
| const data = (await res.json()) as { error?: string }; | ||
| throw new Error(data.error ?? 'Reset password failed'); | ||
| } | ||
| }; | ||
|
Comment on lines
+191
to
212
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm the Treaty path for the new password-reset routes (Elysia exposes the prefix as a key).
rg -nP -C2 "prefix:\s*'/password-reset'" packages/api/src/routes
rg -nP -C3 'apiClient\.' apps/expo --type=ts | head -50Repository: PackRat-AI/PackRat Length of output: 4537 🏁 Script executed: # Get the full passwordReset routes structure to understand endpoint paths
cat -n packages/api/src/routes/passwordReset.tsRepository: PackRat-AI/PackRat Length of output: 1944 🏁 Script executed: # Check the useAuthActions.ts file to confirm direct fetch() usage
sed -n '180,220p' apps/expo/features/auth/hooks/useAuthActions.ts | cat -nRepository: PackRat-AI/PackRat Length of output: 2164 🏁 Script executed: # Verify how the apiClient is imported and used in the auth module
grep -n "apiClient" apps/expo/features/auth/hooks/useAuthActions.ts | head -20Repository: PackRat-AI/PackRat Length of output: 44 🏁 Script executed: # Check imports and top of useAuthActions.ts file
head -40 apps/expo/features/auth/hooks/useAuthActions.ts | cat -nRepository: PackRat-AI/PackRat Length of output: 1821 🏁 Script executed: # Search for apiClient definition in the auth feature
grep -rn "apiClient" apps/expo/features/auth/ --type=tsRepository: PackRat-AI/PackRat Length of output: 183 🏁 Script executed: # Search for apiClient in the expo app using rg
rg -n "import.*apiClient" apps/expo/features/ | head -20Repository: PackRat-AI/PackRat Length of output: 2277 🏁 Script executed: # Check if apiClient exists and how it's initialized
rg -n "apiClient\s*=" apps/expo/lib --type ts -A 3 | head -40Repository: PackRat-AI/PackRat Length of output: 365 🏁 Script executed: # Verify passwordReset.ts export and how routes are structured
grep -n "export\|\.post\|\.get" packages/api/src/routes/passwordReset.tsRepository: PackRat-AI/PackRat Length of output: 165 🏁 Script executed: # Check the apiClient definition and what routes it has
cat -n apps/expo/lib/api/packrat.ts | head -60Repository: PackRat-AI/PackRat Length of output: 2727 🏁 Script executed: # Check what Treaty routes are available in the apiClient by looking at the type/implementation
rg -n "password-reset|passwordReset" apps/expo/lib/api/ --type ts -B 2 -A 2Repository: PackRat-AI/PackRat Length of output: 44 🏁 Script executed: # Verify the API schema for password reset
rg -n "ForgotPasswordRequestSchema|ResetPasswordRequestSchema" packages/api/src --type ts -B 1 -A 3Repository: PackRat-AI/PackRat Length of output: 1711 🏁 Script executed: # Check how createApiClient is implemented and what response format it uses
rg -n "createApiClient\|export.*function createApiClient" packages/api-client --type ts -A 10 | head -50Repository: PackRat-AI/PackRat Length of output: 44 🏁 Script executed: # Look at how other features handle apiClient error responses
rg -n "const.*error.*await apiClient" apps/expo/features --type ts -A 2 | head -40Repository: PackRat-AI/PackRat Length of output: 3314 🏁 Script executed: # Check how the apiClient is generated and if password-reset routes are registered
rg -n "password-reset|passwordReset" packages/api-client --type ts -B 3 -A 3Repository: PackRat-AI/PackRat Length of output: 44 🏁 Script executed: # Look at the main API index to see how routes are structured and exported
cat -n packages/api/src/index.ts | head -80Repository: PackRat-AI/PackRat Length of output: 3517 🏁 Script executed: # Verify the type of apiClient to see available routes
rg -n "typeof.*apiClient|type.*Api.*=" apps/expo/lib/api --type ts -B 2 -A 5Repository: PackRat-AI/PackRat Length of output: 206 🏁 Script executed: # Check the routes index to see if passwordResetRoutes are exported
cat -n packages/api/src/routes/index.tsRepository: PackRat-AI/PackRat Length of output: 1966 🏁 Script executed: # Look at how routes are structured and composed
rg -n "export.*routes\|passwordResetRoutes" packages/api/src/routes --type tsRepository: PackRat-AI/PackRat Length of output: 44 Use the typed API client, not raw The Expo app's coding guidelines explicitly forbid direct Update the functions to: const { error } = await apiClient['password-reset'].request.post({ email });
if (error) throw new Error((error.value as { error?: string })?.error ?? 'Forgot password failed');const { error } = await apiClient['password-reset'].verify.post({
email,
code: opts.token,
newPassword: opts.newPassword,
});
if (error) throw new Error((error.value as { error?: string })?.error ?? 'Reset password failed');This also fixes a latent bug in the current code: 🤖 Prompt for AI Agents |
||
|
|
||
| const verifyEmail = async (_email: string, token: string) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,20 +1,52 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { createApiClient } from '@packrat/api-client'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { clientEnvs } from '@packrat/env/expo-client'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { fromZod } from '@packrat/guards'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { store } from 'expo-app/atoms/store'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { needsReauthAtom } from 'expo-app/features/auth/atoms/authAtoms'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { authClient } from 'expo-app/lib/auth-client'; | ||||||||||||||||||||||||||||||||||||||||||||
| import * as SecureStore from 'expo-secure-store'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // The expoClient plugin serialises all cookies into SecureStore under this key. | ||||||||||||||||||||||||||||||||||||||||||||
| // Parsing it locally avoids a network round-trip on every API request. | ||||||||||||||||||||||||||||||||||||||||||||
| const COOKIE_STORE_KEY = 'packrat_cookie'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const CookieStoreSchema = z.record(z.object({ value: z.string() })); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // expoClient stores cookies as JSON: { "better-auth.session_token": { value, expires } } | ||||||||||||||||||||||||||||||||||||||||||||
| function parseSessionToken(cookieJson: string | null): string | null { | ||||||||||||||||||||||||||||||||||||||||||||
| console.log('[auth] Parsing session token from cookie string:', cookieJson); | ||||||||||||||||||||||||||||||||||||||||||||
| if (!cookieJson) return null; | ||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
| const cookies = fromZod(CookieStoreSchema)(JSON.parse(cookieJson)); | ||||||||||||||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||||||||||||||
| '[auth] Parsed session token from cookie string:', | ||||||||||||||||||||||||||||||||||||||||||||
| cookies?.['better-auth.session_token']?.value ?? 'null', | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| return cookies?.['better-auth.session_token']?.value ?? null; | ||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||
| console.warn('[auth] Failed to parse session token from cookie string:', err); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove session-token logging — this is a secret leak.
🔒 Proposed fix function parseSessionToken(cookieJson: string | null): string | null {
- console.log('[auth] Parsing session token from cookie string:', cookieJson);
if (!cookieJson) return null;
try {
const cookies = JSON.parse(cookieJson) as Record<string, { value: string }>;
- console.log(
- '[auth] Parsed session token from cookie string:',
- cookies['better-auth.session_token']?.value ?? 'null',
- );
return cookies['better-auth.session_token']?.value ?? null;
} catch (err) {
console.warn('[auth] Failed to parse session token from cookie string:', err);
return null;
}
}📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: Checks / 0_checks.txt[error] 17-17: bun check:casts:strict failed. Unsafe type cast(s) found. Replace 'JSON.parse(cookieJson) as Record<string, { value: string }>' with guards from 🪛 GitHub Actions: Checks / checks[error] 17-17: bun check:casts:strict failed. Found 1 unsafe type cast(s). Replace casts with guards from 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Respect the cookie's The stored payload includes ♻️ Proposed fix- const cookies = JSON.parse(cookieJson) as Record<string, { value: string }>;
- return cookies['better-auth.session_token']?.value ?? null;
+ const cookies = JSON.parse(cookieJson) as Record<string, { value: string; expires?: string }>;
+ const entry = cookies['better-auth.session_token'];
+ if (!entry) return null;
+ if (entry.expires && Date.parse(entry.expires) <= Date.now()) return null;
+ return entry.value ?? null;(Once you switch to a 🧰 Tools🪛 GitHub Actions: Checks / 0_checks.txt[error] 17-17: bun check:casts:strict failed. Unsafe type cast(s) found. Replace 'JSON.parse(cookieJson) as Record<string, { value: string }>' with guards from 🪛 GitHub Actions: Checks / checks[error] 17-17: bun check:casts:strict failed. Found 1 unsafe type cast(s). Replace casts with guards from 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const apiClient = createApiClient({ | ||||||||||||||||||||||||||||||||||||||||||||
| baseUrl: clientEnvs.EXPO_PUBLIC_API_URL, | ||||||||||||||||||||||||||||||||||||||||||||
| auth: { | ||||||||||||||||||||||||||||||||||||||||||||
| // Read the token from SecureStore — no network call on every API request. | ||||||||||||||||||||||||||||||||||||||||||||
| getAccessToken: async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const { data } = await authClient.getSession(); | ||||||||||||||||||||||||||||||||||||||||||||
| return data?.session?.token ?? null; | ||||||||||||||||||||||||||||||||||||||||||||
| const cookieStr = await SecureStore.getItemAsync(COOKIE_STORE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||
| return parseSessionToken(cookieStr); | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| // Better Auth manages session renewal internally — no separate refresh token flow. | ||||||||||||||||||||||||||||||||||||||||||||
| // Better Auth has no separate refresh-token endpoint; the 7-day session | ||||||||||||||||||||||||||||||||||||||||||||
| // token is the only credential. Returning null here is intentional. | ||||||||||||||||||||||||||||||||||||||||||||
| getRefreshToken: () => null, | ||||||||||||||||||||||||||||||||||||||||||||
| onAccessTokenRefreshed: () => {}, | ||||||||||||||||||||||||||||||||||||||||||||
| onNeedsReauth: () => { | ||||||||||||||||||||||||||||||||||||||||||||
| onNeedsReauth: async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| // A 401 can be transient (e.g. the server briefly returned an error). | ||||||||||||||||||||||||||||||||||||||||||||
| // Verify the session is actually gone before alarming the user. | ||||||||||||||||||||||||||||||||||||||||||||
| const { data } = await authClient.getSession(); | ||||||||||||||||||||||||||||||||||||||||||||
| if (data?.session) return; | ||||||||||||||||||||||||||||||||||||||||||||
| store.set(needsReauthAtom, true); | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { ForgotPasswordRequestSchema, ResetPasswordRequestSchema } from '@packrat/api/schemas/auth'; | ||
| import { | ||
| requestPasswordReset, | ||
| verifyOtpAndResetPassword, | ||
| } from '@packrat/api/services/passwordResetService'; | ||
| import { Elysia, status } from 'elysia'; | ||
|
|
||
| export const passwordResetRoutes = new Elysia({ prefix: '/password-reset' }) | ||
| // public-route: unauthenticated users need this to initiate a password reset | ||
| .post( | ||
| '/request', | ||
| async ({ body }) => { | ||
| await requestPasswordReset(body.email); | ||
| return { success: true, message: 'If an account exists, a reset code has been sent.' }; | ||
| }, | ||
| { | ||
| body: ForgotPasswordRequestSchema, | ||
| detail: { | ||
| tags: ['Auth'], | ||
| summary: 'Request password reset', | ||
| description: | ||
| 'Send a 6-digit OTP to the user email. Always returns success to prevent email enumeration.', | ||
| }, | ||
| }, | ||
| ) | ||
| // public-route: unauthenticated users need this to verify OTP and set a new password | ||
| .post( | ||
| '/verify', | ||
| async ({ body }) => { | ||
| try { | ||
| await verifyOtpAndResetPassword(body); | ||
| return { success: true, message: 'Password reset successfully.' }; | ||
| } catch (error) { | ||
| return status(400, { | ||
| error: error instanceof Error ? error.message : 'Password reset failed', | ||
| }); | ||
| } | ||
|
Comment on lines
+29
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return a generic error message on verify failure. Reflecting 🔒 Proposed fix- } catch (error) {
- return status(400, {
- error: error instanceof Error ? error.message : 'Password reset failed',
- });
+ } catch (error) {
+ console.error('[password-reset] verify failed:', error);
+ return status(400, { error: 'Invalid or expired reset code' });
}🤖 Prompt for AI Agents |
||
| }, | ||
| { | ||
| body: ResetPasswordRequestSchema, | ||
| detail: { | ||
| tags: ['Auth'], | ||
| summary: 'Verify OTP and reset password', | ||
| description: 'Validate the 6-digit OTP and set a new password.', | ||
| }, | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||
| import { hashPassword } from '@better-auth/utils/password'; | ||||||||||||||||||
| import { createDb } from '@packrat/api/db'; | ||||||||||||||||||
| import { account, users, verification } from '@packrat/api/db/schema'; | ||||||||||||||||||
| import { timingSafeEqual } from '@packrat/api/utils/auth'; | ||||||||||||||||||
| import { sendPasswordResetEmail } from '@packrat/api/utils/email'; | ||||||||||||||||||
| import { and, eq, gt } from 'drizzle-orm'; | ||||||||||||||||||
|
|
||||||||||||||||||
| const OTP_LENGTH = 6; | ||||||||||||||||||
| const OTP_TTL_MS = 15 * 60 * 1000; // 15 minutes | ||||||||||||||||||
| const IDENTIFIER_PREFIX = 'password-reset:'; | ||||||||||||||||||
|
|
||||||||||||||||||
| function generateOtp(): string { | ||||||||||||||||||
| return Array.from({ length: OTP_LENGTH }, () => Math.floor(Math.random() * 10)).join(''); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+12
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a cryptographically secure RNG for the OTP.
🔒 Proposed fix using crypto.getRandomValues function generateOtp(): string {
- return Array.from({ length: OTP_LENGTH }, () => Math.floor(Math.random() * 10)).join('');
+ const bytes = new Uint8Array(OTP_LENGTH);
+ crypto.getRandomValues(bytes);
+ return Array.from(bytes, (b) => (b % 10).toString()).join('');
}Note: 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| export async function requestPasswordReset(email: string): Promise<void> { | ||||||||||||||||||
| const db = createDb(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const user = await db.query.users.findFirst({ where: eq(users.email, email) }); | ||||||||||||||||||
| if (!user) return; // Don't reveal whether the email is registered | ||||||||||||||||||
|
|
||||||||||||||||||
| const code = generateOtp(); | ||||||||||||||||||
| const identifier = `${IDENTIFIER_PREFIX}${email}`; | ||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||
| const expiresAt = new Date(now.getTime() + OTP_TTL_MS); | ||||||||||||||||||
|
|
||||||||||||||||||
| await db.delete(verification).where(eq(verification.identifier, identifier)); | ||||||||||||||||||
| await db.insert(verification).values({ | ||||||||||||||||||
| id: crypto.randomUUID(), | ||||||||||||||||||
| identifier, | ||||||||||||||||||
| value: code, | ||||||||||||||||||
| expiresAt, | ||||||||||||||||||
| createdAt: now, | ||||||||||||||||||
| updatedAt: now, | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| await sendPasswordResetEmail({ to: email, code }); | ||||||||||||||||||
|
Comment on lines
+27
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Store OTPs hashed and add a per-OTP attempt limit. Two issues here:
Together, these turn a small theoretical risk into a practical attack surface. Also consider rate-limiting 🤖 Prompt for AI Agents |
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export async function verifyOtpAndResetPassword({ | ||||||||||||||||||
| email, | ||||||||||||||||||
| code, | ||||||||||||||||||
| newPassword, | ||||||||||||||||||
| }: { | ||||||||||||||||||
| email: string; | ||||||||||||||||||
| code: string; | ||||||||||||||||||
| newPassword: string; | ||||||||||||||||||
| }): Promise<void> { | ||||||||||||||||||
| const db = createDb(); | ||||||||||||||||||
| const identifier = `${IDENTIFIER_PREFIX}${email}`; | ||||||||||||||||||
|
|
||||||||||||||||||
| const record = await db.query.verification.findFirst({ | ||||||||||||||||||
| where: and(eq(verification.identifier, identifier), gt(verification.expiresAt, new Date())), | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!record || !timingSafeEqual(record.value, code)) { | ||||||||||||||||||
| throw new Error('Invalid or expired reset code'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const user = await db.query.users.findFirst({ where: eq(users.email, email) }); | ||||||||||||||||||
| if (!user) throw new Error('User not found'); | ||||||||||||||||||
|
|
||||||||||||||||||
| const hashedPassword = await hashPassword(newPassword); | ||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Update the credential account record (Better Auth email/password users) | ||||||||||||||||||
| const updated = await db | ||||||||||||||||||
| .update(account) | ||||||||||||||||||
| .set({ password: hashedPassword, updatedAt: now }) | ||||||||||||||||||
| .where(and(eq(account.userId, user.id), eq(account.providerId, 'credential'))) | ||||||||||||||||||
| .returning(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Fallback for legacy users whose password lives on the users row | ||||||||||||||||||
| if (updated.length === 0) { | ||||||||||||||||||
| await db | ||||||||||||||||||
| .update(users) | ||||||||||||||||||
| .set({ passwordHash: hashedPassword, updatedAt: now }) | ||||||||||||||||||
| .where(eq(users.id, user.id)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+67
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm passwordHash still exists on the users schema and find any consumers.
rg -nP -C3 'passwordHash' packages/api/src/db
rg -nP -C3 '\bpasswordHash\b' packages/api/src --type=tsRepository: PackRat-AI/PackRat Length of output: 6773 🏁 Script executed: rg -nP -C5 'passwordHash.*eq|passwordHash.*compare|verify.*passwordHash' packages/api/src --type=ts
rg -nP -C5 'credential.*account|provider.*credential' packages/api/src/services --type=ts
rg -nP 'from.*@better-auth' packages/api/src --type=ts | head -20Repository: PackRat-AI/PackRat Length of output: 1887 🏁 Script executed: rg -nP 'verifyPassword' packages/api/src --type=ts -A3 -B3
rg -nP 'signIn.*credential|credential.*signIn|login' packages/api/src --type=ts | head -30Repository: PackRat-AI/PackRat Length of output: 4345 🏁 Script executed: rg -nP 'password.*field|passwordHash.*schema' packages/api/src/auth --type=ts -A2 -B2
cat packages/api/src/auth/auth.config.tsRepository: PackRat-AI/PackRat Length of output: 2970 Remove the Better Auth's emailAndPassword provider exclusively uses the If you need to support legacy users without credential accounts, create the credential account instead of falling back to a field Better Auth won't check. Otherwise, remove this fallback entirely. 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| await db.delete(verification).where(eq(verification.identifier, identifier)); | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hide the decorative Apple glyph from accessibility output.
Line 122 adds a private-use glyph that can be read aloud by VoiceOver. Keep it decorative so the button is announced cleanly from the text label.
Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents