From 674df006c3f6731e1f89bfb6591673dcb4eac42e Mon Sep 17 00:00:00 2001 From: Julian Gomez Date: Wed, 26 Nov 2025 09:58:24 -0600 Subject: [PATCH 1/3] wip --- .../welcome/contents/ResetPassword..tsx | 23 ++++++++++++++++ .../welcome/contents/SignInContent.tsx | 27 ++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 app/components/welcome/contents/ResetPassword..tsx diff --git a/app/components/welcome/contents/ResetPassword..tsx b/app/components/welcome/contents/ResetPassword..tsx new file mode 100644 index 00000000..49ef1c9f --- /dev/null +++ b/app/components/welcome/contents/ResetPassword..tsx @@ -0,0 +1,23 @@ +import { AppOrbitImage } from '../../ui/app-orbit-image' + +type Props = { + onBack: () => void +} + +export default function ResetPassword({ onBack }: Props) { + return ( +
+
+ +
+
+ +
+
+ ) +} diff --git a/app/components/welcome/contents/SignInContent.tsx b/app/components/welcome/contents/SignInContent.tsx index c3941fb5..ae4c6c11 100644 --- a/app/components/welcome/contents/SignInContent.tsx +++ b/app/components/welcome/contents/SignInContent.tsx @@ -19,6 +19,7 @@ import { useDictionaryStore } from '@/app/store/useDictionaryStore' import { AppOrbitImage } from '@/app/components/ui/app-orbit-image' import { STORE_KEYS } from '../../../../lib/constants/store-keys' import { isValidEmail, isStrongPassword } from '@/app/utils/utils' +import ResetPassword from './ResetPassword.' // Auth provider configuration const AUTH_PROVIDERS = { @@ -120,6 +121,7 @@ export default function SignInContent() { const [password, setPassword] = useState('') const [isLoggingIn, setIsLoggingIn] = useState(false) const [errorMessage, setErrorMessage] = useState(null) + const [showResetPassword, setShowResetPassword] = useState(false) const { user, @@ -412,10 +414,14 @@ export default function SignInContent() { return renderSingleProviderOption(userProvider) } + if (showResetPassword) { + return setShowResetPassword(false)} /> + } + return (
{/* Left side - Sign in form */} -
+
{/* Logo */}
@@ -454,11 +460,8 @@ export default function SignInContent() { )} {/* Link to create new account */} -
+

- {userProvider - ? 'Sign in with a different account?' - : 'Need to create an account?'}{' '} +

+

+

@@ -477,7 +490,7 @@ export default function SignInContent() {
{/* Right side - Placeholder for image */} -
+
From 1faa92e202b351d33b0835c2b796f398877a0825 Mon Sep 17 00:00:00 2001 From: Julian Gomez Date: Wed, 26 Nov 2025 14:03:35 -0600 Subject: [PATCH 2/3] Reset password workflows --- .../welcome/contents/EmailLoginContent.tsx | 18 +- .../welcome/contents/ResetPassword..tsx | 23 -- .../welcome/contents/ResetPassword.tsx | 226 ++++++++++++++++++ .../welcome/contents/SignInContent.tsx | 20 +- lib/window/ipcEvents.ts | 9 + scripts/clean-app-data.js | 35 +-- server/src/services/auth0.ts | 63 +++++ 7 files changed, 345 insertions(+), 49 deletions(-) delete mode 100644 app/components/welcome/contents/ResetPassword..tsx create mode 100644 app/components/welcome/contents/ResetPassword.tsx diff --git a/app/components/welcome/contents/EmailLoginContent.tsx b/app/components/welcome/contents/EmailLoginContent.tsx index 37de3bdb..033cd30a 100644 --- a/app/components/welcome/contents/EmailLoginContent.tsx +++ b/app/components/welcome/contents/EmailLoginContent.tsx @@ -3,6 +3,7 @@ import { Button } from '@/app/components/ui/button' import { AppOrbitImage } from '@/app/components/ui/app-orbit-image' import { isValidEmail, isStrongPassword } from '@/app/utils/utils' import { useAuth } from '@/app/components/auth/useAuth' +import ResetPassword from './ResetPassword' type Props = { initialEmail?: string @@ -14,6 +15,7 @@ export default function EmailLoginContent({ initialEmail = '', onBack, }: Props) { + const [showResetPassword, setShowResetPassword] = useState(false) const [email, setEmail] = useState(initialEmail) const [password, setPassword] = useState('') @@ -43,6 +45,15 @@ export default function EmailLoginContent({ } } + if (showResetPassword) { + return ( + setShowResetPassword(false)} + /> + ) + } + return (
{/* Left: form */} @@ -115,9 +126,12 @@ export default function EmailLoginContent({ - +
diff --git a/app/components/welcome/contents/ResetPassword..tsx b/app/components/welcome/contents/ResetPassword..tsx deleted file mode 100644 index 49ef1c9f..00000000 --- a/app/components/welcome/contents/ResetPassword..tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { AppOrbitImage } from '../../ui/app-orbit-image' - -type Props = { - onBack: () => void -} - -export default function ResetPassword({ onBack }: Props) { - return ( -
-
- -
-
- -
-
- ) -} diff --git a/app/components/welcome/contents/ResetPassword.tsx b/app/components/welcome/contents/ResetPassword.tsx new file mode 100644 index 00000000..0cf3f15d --- /dev/null +++ b/app/components/welcome/contents/ResetPassword.tsx @@ -0,0 +1,226 @@ +import { useMemo, useState } from 'react' +import { Button } from '@/app/components/ui/button' +import { AppOrbitImage } from '../../ui/app-orbit-image' +import { STORE_KEYS } from '../../../../lib/constants/store-keys' +import { isValidEmail } from '@/app/utils/utils' + +type Props = { + email?: string + onBack: () => void +} + +export default function ResetPassword({ email, onBack }: Props) { + const storedUser = window.electron?.store?.get(STORE_KEYS.AUTH)?.user + const initialEmail = email || storedUser?.email || '' + + const [editableEmail, setEditableEmail] = useState(initialEmail) + const [isEditingEmail, setIsEditingEmail] = useState(!initialEmail) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [emailSent, setEmailSent] = useState(false) + const [seconds, setSeconds] = useState(0) + const [isResending, setIsResending] = useState(false) + + const emailOk = useMemo(() => isValidEmail(editableEmail), [editableEmail]) + + const handleContinue = async () => { + if (!editableEmail || !emailOk) { + setError('Please enter a valid email address') + return + } + + try { + setIsLoading(true) + setError(null) + + const res = await window.api.invoke('auth0-reset-password', { + email: editableEmail, + }) + + if (res?.success) { + setEmailSent(true) + setSeconds(30) + // Start countdown + const id = setInterval(() => { + setSeconds(s => { + if (s <= 1) { + clearInterval(id) + return 0 + } + return s - 1 + }) + }, 1000) + } else { + setError(res?.error || 'Failed to send reset email') + } + } catch (e: any) { + setError(e?.message || 'An error occurred') + } finally { + setIsLoading(false) + } + } + + const handleResend = async () => { + if (seconds > 0 || isResending) return + + try { + setIsResending(true) + setError(null) + + const res = await window.api.invoke('auth0-reset-password', { + email: editableEmail, + }) + + if (res?.success) { + setSeconds(30) + const id = setInterval(() => { + setSeconds(s => { + if (s <= 1) { + clearInterval(id) + return 0 + } + return s - 1 + }) + }, 1000) + } else { + setError(res?.error || 'Failed to resend reset email') + } + } catch (e: any) { + setError(e?.message || 'An error occurred') + } finally { + setIsResending(false) + } + } + + const handleOpenEmailApp = () => { + window.api.invoke('web-open-url', 'mailto:') + } + + // Check Your Inbox view (after email sent) + if (emailSent) { + return ( +
+ {/* Left content */} +
+ + +
+

+ Check Your Inbox +

+

+ We've sent a reset link to {editableEmail}. +

+

+ Follow the instructions in the email to set a new password. +

+
+ + + + + + {error && ( +

{error}

+ )} +
+ + {/* Right illustration */} +
+ +
+
+ ) + } + + // Initial Reset Password view + return ( +
+ {/* Left content */} +
+ + +
+

+ Reset Password +

+ {isEditingEmail ? ( +
+ + setEditableEmail(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter' && emailOk) { + e.preventDefault() + handleContinue() + } + }} + className="mt-2 h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground" + /> +
+ ) : ( +

+ We'll send a reset link to {editableEmail}.{' '} + +

+ )} +
+ + + + {error && ( +

{error}

+ )} +
+ + {/* Right illustration */} +
+ +
+
+ ) +} diff --git a/app/components/welcome/contents/SignInContent.tsx b/app/components/welcome/contents/SignInContent.tsx index ae4c6c11..f5df6912 100644 --- a/app/components/welcome/contents/SignInContent.tsx +++ b/app/components/welcome/contents/SignInContent.tsx @@ -19,7 +19,7 @@ import { useDictionaryStore } from '@/app/store/useDictionaryStore' import { AppOrbitImage } from '@/app/components/ui/app-orbit-image' import { STORE_KEYS } from '../../../../lib/constants/store-keys' import { isValidEmail, isStrongPassword } from '@/app/utils/utils' -import ResetPassword from './ResetPassword.' +import ResetPassword from './ResetPassword' // Auth provider configuration const AUTH_PROVIDERS = { @@ -477,14 +477,16 @@ export default function SignInContent() { : 'Create account'}

-

- -

+ {(!userProvider || userProvider === 'email') && ( +

+ +

+ )}
diff --git a/lib/window/ipcEvents.ts b/lib/window/ipcEvents.ts index 9e63d809..502b1ab1 100644 --- a/lib/window/ipcEvents.ts +++ b/lib/window/ipcEvents.ts @@ -421,6 +421,15 @@ export function registerIPC() { }) }) + // Send password reset email via server proxy + handleIPC('auth0-reset-password', async (_e, { email }) => { + if (!email) return { success: false, error: 'Missing email' } + return itoHttpClient.post('/auth0/reset-password', { + email, + connection: Auth0Connections.database, + }) + }) + // Check if email exists for db signup and whether it's verified (via server proxy) handleIPC('auth0-check-email', async (_e, { email }) => { if (!email) return { success: false, error: 'Missing email' } diff --git a/scripts/clean-app-data.js b/scripts/clean-app-data.js index dafca0f0..20831112 100644 --- a/scripts/clean-app-data.js +++ b/scripts/clean-app-data.js @@ -5,22 +5,27 @@ const fs = require('fs') const path = require('path') const platform = os.platform() -let appDataPath +const appNames = ['Ito-dev', 'Ito-local', 'Ito-prod', 'Ito'] -if (platform === 'darwin') { - appDataPath = path.join(os.homedir(), 'Library', 'Application Support', 'Ito') -} else if (platform === 'win32') { - appDataPath = path.join( - process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), - 'Ito', - ) -} else { - appDataPath = path.join(os.homedir(), '.config', 'ito') +function getAppDataPath(appName) { + if (platform === 'darwin') { + return path.join(os.homedir(), 'Library', 'Application Support', appName) + } else if (platform === 'win32') { + return path.join( + process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), + appName, + ) + } else { + return path.join(os.homedir(), '.config', appName.toLowerCase()) + } } -if (fs.existsSync(appDataPath)) { - fs.rmSync(appDataPath, { recursive: true, force: true }) - console.log(`✓ Removed app data from: ${appDataPath}`) -} else { - console.log(`ℹ No app data found at: ${appDataPath}`) +for (const appName of appNames) { + const appDataPath = getAppDataPath(appName) + if (fs.existsSync(appDataPath)) { + fs.rmSync(appDataPath, { recursive: true, force: true }) + console.log(`✓ Removed app data from: ${appDataPath}`) + } else { + console.log(`ℹ No app data found at: ${appDataPath}`) + } } diff --git a/server/src/services/auth0.ts b/server/src/services/auth0.ts index 5eb3d5e9..f33e31ec 100644 --- a/server/src/services/auth0.ts +++ b/server/src/services/auth0.ts @@ -5,6 +5,11 @@ type SendVerificationBody = { clientId?: string } +type ResetPasswordBody = { + email?: string + connection?: string +} + export const registerAuth0Routes = async (fastify: FastifyInstance) => { const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN const AUTH0_MGMT_CLIENT_ID = process.env.AUTH0_MGMT_CLIENT_ID @@ -159,4 +164,62 @@ export const registerAuth0Routes = async (fastify: FastifyInstance) => { .send({ success: false, error: error?.message || 'Network error' }) } }) + + fastify.post('/auth0/reset-password', async (request, reply) => { + const body = (request.body as ResetPasswordBody) || {} + const { email, connection = 'Username-Password-Authentication' } = body + + if (!email) { + reply.status(400).send({ success: false, error: 'Missing email' }) + return + } + + if (!AUTH0_DOMAIN) { + reply + .status(500) + .send({ success: false, error: 'AUTH0_DOMAIN not configured' }) + return + } + + const clientId = process.env.AUTH0_CLIENT_ID + if (!clientId) { + reply + .status(500) + .send({ success: false, error: 'AUTH0_CLIENT_ID not configured' }) + return + } + + try { + const url = `https://${AUTH0_DOMAIN}/dbconnections/change_password` + const res = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + client_id: clientId, + email, + connection, + }), + }) + + if (res.ok) { + reply.send({ success: true, message: 'Password reset email sent' }) + return + } + + let data: any + try { + data = await res.json() + } catch { + data = { error: await res.text() } + } + + const message = + data?.error_description || data?.error || `Reset failed (${res.status})` + reply.status(res.status).send({ success: false, error: message }) + } catch (error: any) { + reply + .status(500) + .send({ success: false, error: error?.message || 'Network error' }) + } + }) } From e70a667b4e1289d38816797ea4bcbf57a98ac7f1 Mon Sep 17 00:00:00 2001 From: Julian Gomez Date: Wed, 26 Nov 2025 14:10:36 -0600 Subject: [PATCH 3/3] cleanup --- server/src/services/auth0.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/server/src/services/auth0.ts b/server/src/services/auth0.ts index f33e31ec..3300e23a 100644 --- a/server/src/services/auth0.ts +++ b/server/src/services/auth0.ts @@ -12,12 +12,18 @@ type ResetPasswordBody = { export const registerAuth0Routes = async (fastify: FastifyInstance) => { const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN + const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID const AUTH0_MGMT_CLIENT_ID = process.env.AUTH0_MGMT_CLIENT_ID const AUTH0_MGMT_CLIENT_SECRET = process.env.AUTH0_MGMT_CLIENT_SECRET if (!AUTH0_DOMAIN) { fastify.log.error('AUTH0_DOMAIN is not set') } + if (!AUTH0_CLIENT_ID) { + fastify.log.warn( + 'AUTH0_CLIENT_ID is not set; reset-password route will fail', + ) + } if (!AUTH0_MGMT_CLIENT_ID || !AUTH0_MGMT_CLIENT_SECRET) { fastify.log.warn( 'Auth0 management client credentials are not fully set; management routes will fail', @@ -173,19 +179,8 @@ export const registerAuth0Routes = async (fastify: FastifyInstance) => { reply.status(400).send({ success: false, error: 'Missing email' }) return } - - if (!AUTH0_DOMAIN) { - reply - .status(500) - .send({ success: false, error: 'AUTH0_DOMAIN not configured' }) - return - } - - const clientId = process.env.AUTH0_CLIENT_ID - if (!clientId) { - reply - .status(500) - .send({ success: false, error: 'AUTH0_CLIENT_ID not configured' }) + if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID) { + reply.status(500).send({ success: false, error: 'Auth0 not configured' }) return } @@ -195,14 +190,14 @@ export const registerAuth0Routes = async (fastify: FastifyInstance) => { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ - client_id: clientId, + client_id: AUTH0_CLIENT_ID, email, connection, }), }) if (res.ok) { - reply.send({ success: true, message: 'Password reset email sent' }) + reply.send({ success: true }) return } @@ -212,7 +207,6 @@ export const registerAuth0Routes = async (fastify: FastifyInstance) => { } catch { data = { error: await res.text() } } - const message = data?.error_description || data?.error || `Reset failed (${res.status})` reply.status(res.status).send({ success: false, error: message })