feat: combine email verification and device linking into single flow
- Modified handleRequestDeviceLink to accept unverified/new emails - User can now create account via email on any device - handleLinkDevice now also verifies email when completing device link - Updated frontend CryptID.tsx with contextual messaging - Added needsUsername flow for new account creation via email - One click now verifies email AND links device, saving user steps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
be6f58630f
commit
d672275adf
|
|
@ -29,6 +29,10 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
const [suggestedUsername, setSuggestedUsername] = useState<string>('');
|
||||
const [emailLinkSent, setEmailLinkSent] = useState(false);
|
||||
const [pendingCryptId, setPendingCryptId] = useState<string | null>(null);
|
||||
const [needsUsername, setNeedsUsername] = useState(false);
|
||||
const [newAccountUsername, setNewAccountUsername] = useState('');
|
||||
const [isNewAccountFlow, setIsNewAccountFlow] = useState(false);
|
||||
const [needsEmailVerificationFlow, setNeedsEmailVerificationFlow] = useState(false);
|
||||
const [browserSupport, setBrowserSupport] = useState<{
|
||||
supported: boolean;
|
||||
secure: boolean;
|
||||
|
|
@ -130,15 +134,24 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
|
||||
/**
|
||||
* Handle email link request (Device B - new device)
|
||||
* Combined flow: This will verify email AND link device in one step
|
||||
*/
|
||||
const handleEmailLinkRequest = async () => {
|
||||
if (!email) return;
|
||||
|
||||
// If we need a username but don't have one, show the username form
|
||||
if (needsUsername && !newAccountUsername.trim()) {
|
||||
setError('Please enter a CryptID username to create your account');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await requestDeviceLink(email);
|
||||
const result = await requestDeviceLink(email, {
|
||||
cryptidUsername: needsUsername ? newAccountUsername.trim() : undefined
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
if (result.alreadyLinked) {
|
||||
|
|
@ -155,12 +168,29 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
// Email sent - show waiting message
|
||||
setEmailLinkSent(true);
|
||||
setPendingCryptId(result.cryptidUsername || null);
|
||||
addNotification('Verification email sent! Check your inbox.', 'success');
|
||||
setNeedsUsername(false);
|
||||
setIsNewAccountFlow(result.isNewAccount || false);
|
||||
setNeedsEmailVerificationFlow(result.needsEmailVerification || false);
|
||||
|
||||
// Contextual message based on flow type
|
||||
if (result.isNewAccount) {
|
||||
addNotification('Setup email sent! Check your inbox to complete registration.', 'success');
|
||||
} else if (result.needsEmailVerification) {
|
||||
addNotification('Verification email sent! This will verify your email and link this device.', 'success');
|
||||
} else {
|
||||
addNotification('Device link email sent! Check your inbox.', 'success');
|
||||
}
|
||||
} else {
|
||||
setError('Failed to send verification email');
|
||||
}
|
||||
} else {
|
||||
setError(result.error || 'Failed to request device link');
|
||||
// Handle needsUsername response from server
|
||||
if (result.needsUsername) {
|
||||
setNeedsUsername(true);
|
||||
setError('No account found for this email. Please choose a CryptID username to create your account.');
|
||||
} else {
|
||||
setError(result.error || 'Failed to request device link');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Email link request error:', err);
|
||||
|
|
@ -252,21 +282,41 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
|
||||
// Email link sent - waiting for user to click verification
|
||||
if (authMode === 'email-link' && emailLinkSent) {
|
||||
// Determine contextual messaging
|
||||
const getHeadline = () => {
|
||||
if (isNewAccountFlow) return 'Complete Your Setup';
|
||||
if (needsEmailVerificationFlow) return 'Verify & Connect';
|
||||
return 'Check Your Email';
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
if (isNewAccountFlow) {
|
||||
return 'Click the link in the email to verify your email and set up your CryptID account on this device.';
|
||||
}
|
||||
if (needsEmailVerificationFlow) {
|
||||
return 'Click the link to verify your email and connect this device to your account in one step.';
|
||||
}
|
||||
return 'Click the link in the email to complete sign in on this device.';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="crypto-login-container">
|
||||
<h2>Check Your Email</h2>
|
||||
<h2>{getHeadline()}</h2>
|
||||
<div className="email-link-pending">
|
||||
<div className="email-icon">📧</div>
|
||||
<p>We sent a verification link to:</p>
|
||||
<p className="email-address"><strong>{email}</strong></p>
|
||||
{pendingCryptId && (
|
||||
<p className="cryptid-info">
|
||||
This will link this device to CryptID: <strong>{pendingCryptId}</strong>
|
||||
{isNewAccountFlow
|
||||
? <>Your CryptID will be: <strong>{pendingCryptId}</strong></>
|
||||
: <>This will link this device to CryptID: <strong>{pendingCryptId}</strong></>
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<p className="email-instructions">
|
||||
Click the link in the email to complete sign in on this device.
|
||||
The link expires in 1 hour.
|
||||
{getDescription()}
|
||||
{' '}The link expires in 1 hour.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -274,6 +324,8 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
setAuthMode('login');
|
||||
setEmail('');
|
||||
setPendingCryptId(null);
|
||||
setIsNewAccountFlow(false);
|
||||
setNeedsEmailVerificationFlow(false);
|
||||
}}
|
||||
className="toggle-button"
|
||||
>
|
||||
|
|
@ -293,11 +345,12 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
if (authMode === 'email-link') {
|
||||
return (
|
||||
<div className="crypto-login-container">
|
||||
<h2>Sign In with Email</h2>
|
||||
<h2>{needsUsername ? 'Create CryptID Account' : 'Sign In with Email'}</h2>
|
||||
<div className="crypto-info">
|
||||
<p>
|
||||
Enter the email address linked to your CryptID account.
|
||||
We'll send a verification link to complete sign in on this device.
|
||||
{needsUsername
|
||||
? 'No account found for this email. Choose a CryptID username to create your account.'
|
||||
: 'Enter your email address to sign in or create an account. One click will verify your email and link this device.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -308,22 +361,48 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
// Reset needsUsername when email changes
|
||||
if (needsUsername) {
|
||||
setNeedsUsername(false);
|
||||
setNewAccountUsername('');
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your email..."
|
||||
required
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || needsUsername}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{needsUsername && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="newAccountUsername">CryptID Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newAccountUsername"
|
||||
value={newAccountUsername}
|
||||
onChange={(e) => setNewAccountUsername(e.target.value)}
|
||||
placeholder="Choose a username..."
|
||||
required
|
||||
disabled={isLoading}
|
||||
autoComplete="username"
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
/>
|
||||
<p className="field-hint">This will be your unique CryptID identity</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !email.trim()}
|
||||
disabled={isLoading || !email.trim() || (needsUsername && !newAccountUsername.trim())}
|
||||
className="crypto-auth-button"
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Send Verification Link'}
|
||||
{isLoading ? 'Sending...' : needsUsername ? 'Create Account & Send Link' : 'Send Verification Link'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
|
@ -333,6 +412,8 @@ const CryptID: React.FC<CryptIDProps> = ({ onSuccess, onCancel }) => {
|
|||
setAuthMode('login');
|
||||
setError(null);
|
||||
setEmail('');
|
||||
setNeedsUsername(false);
|
||||
setNewAccountUsername('');
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className="toggle-button"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ export interface DeviceLinkResult {
|
|||
alreadyLinked?: boolean;
|
||||
emailSent?: boolean;
|
||||
error?: string;
|
||||
// Combined flow fields
|
||||
isNewAccount?: boolean;
|
||||
needsEmailVerification?: boolean;
|
||||
needsUsername?: boolean;
|
||||
// Completion fields
|
||||
emailVerified?: boolean;
|
||||
emailWasJustVerified?: boolean;
|
||||
}
|
||||
|
||||
export interface LookupResult {
|
||||
|
|
@ -130,16 +137,21 @@ export async function checkEmailStatus(cryptidUsername: string): Promise<LookupR
|
|||
* Request to link a new device using email
|
||||
* Called from Device B (new device)
|
||||
*
|
||||
* Flow:
|
||||
* Combined Flow:
|
||||
* 1. Generate new keypair on Device B
|
||||
* 2. Send email + publicKey to server
|
||||
* 2. Send email + publicKey (+ optional username for new accounts) to server
|
||||
* 3. Server sends verification email
|
||||
* 4. User clicks link in email (on Device B)
|
||||
* 5. Device B's key is linked to the account
|
||||
* 5. Device B's key is linked AND email is verified in one step
|
||||
*
|
||||
* If email is not found and no username provided, server returns needsUsername: true
|
||||
*/
|
||||
export async function requestDeviceLink(
|
||||
email: string,
|
||||
deviceName?: string
|
||||
options?: {
|
||||
deviceName?: string;
|
||||
cryptidUsername?: string; // Required for new accounts
|
||||
}
|
||||
): Promise<DeviceLinkResult & { publicKey?: string }> {
|
||||
try {
|
||||
// Generate a new keypair for this device
|
||||
|
|
@ -168,7 +180,8 @@ export async function requestDeviceLink(
|
|||
body: JSON.stringify({
|
||||
email,
|
||||
publicKey,
|
||||
deviceName: deviceName || getDeviceName()
|
||||
deviceName: options?.deviceName || getDeviceName(),
|
||||
cryptidUsername: options?.cryptidUsername
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -177,7 +190,8 @@ export async function requestDeviceLink(
|
|||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.error || 'Failed to request device link'
|
||||
error: data.error || 'Failed to request device link',
|
||||
needsUsername: data.needsUsername
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +203,8 @@ export async function requestDeviceLink(
|
|||
email,
|
||||
publicKey,
|
||||
cryptidUsername: data.cryptidUsername,
|
||||
isNewAccount: data.isNewAccount,
|
||||
needsEmailVerification: data.needsEmailVerification,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
|
@ -408,7 +424,12 @@ export function hasPendingDeviceLink(): boolean {
|
|||
/**
|
||||
* Get pending device link info
|
||||
*/
|
||||
export function getPendingDeviceLink(): { email: string; cryptidUsername: string } | null {
|
||||
export function getPendingDeviceLink(): {
|
||||
email: string;
|
||||
cryptidUsername: string;
|
||||
isNewAccount?: boolean;
|
||||
needsEmailVerification?: boolean;
|
||||
} | null {
|
||||
const pending = sessionStorage.getItem('pendingDeviceLink');
|
||||
if (!pending) return null;
|
||||
|
||||
|
|
@ -417,7 +438,9 @@ export function getPendingDeviceLink(): { email: string; cryptidUsername: string
|
|||
if (Date.now() - data.timestamp < 60 * 60 * 1000) {
|
||||
return {
|
||||
email: data.email,
|
||||
cryptidUsername: data.cryptidUsername
|
||||
cryptidUsername: data.cryptidUsername,
|
||||
isNewAccount: data.isNewAccount,
|
||||
needsEmailVerification: data.needsEmailVerification
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -264,7 +264,10 @@ export async function handleVerifyEmail(
|
|||
/**
|
||||
* Request to link a new device (Device B enters email)
|
||||
* POST /auth/request-device-link
|
||||
* Body: { email, publicKey, deviceName }
|
||||
* Body: { email, publicKey, deviceName, cryptidUsername? }
|
||||
*
|
||||
* Combined flow: If email not verified yet, this will verify email AND link device
|
||||
* in one step when user clicks the link. Saves user time vs separate flows.
|
||||
*/
|
||||
export async function handleRequestDeviceLink(
|
||||
request: Request,
|
||||
|
|
@ -275,9 +278,10 @@ export async function handleRequestDeviceLink(
|
|||
email: string;
|
||||
publicKey: string;
|
||||
deviceName?: string;
|
||||
cryptidUsername?: string; // Optional: for new accounts being set up
|
||||
};
|
||||
|
||||
const { email, publicKey, deviceName } = body;
|
||||
const { email, publicKey, deviceName, cryptidUsername: providedUsername } = body;
|
||||
|
||||
if (!email || !publicKey) {
|
||||
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
||||
|
|
@ -286,6 +290,15 @@ export async function handleRequestDeviceLink(
|
|||
});
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid email format' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const db = env.CRYPTID_DB;
|
||||
if (!db) {
|
||||
return new Response(JSON.stringify({ error: 'Database not configured' }), {
|
||||
|
|
@ -294,34 +307,56 @@ export async function handleRequestDeviceLink(
|
|||
});
|
||||
}
|
||||
|
||||
// Check if email exists and is verified
|
||||
const user = await db.prepare(
|
||||
'SELECT * FROM users WHERE email = ? AND email_verified = 1'
|
||||
// Check if this public key is already registered
|
||||
const existingKey = await db.prepare(
|
||||
'SELECT dk.*, u.cryptid_username FROM device_keys dk JOIN users u ON dk.user_id = u.id WHERE dk.public_key = ?'
|
||||
).bind(publicKey).first<DeviceKey & { cryptid_username: string }>();
|
||||
|
||||
if (existingKey) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Device already linked',
|
||||
cryptidUsername: existingKey.cryptid_username,
|
||||
alreadyLinked: true
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Check if email exists in system (verified or not)
|
||||
let user = await db.prepare(
|
||||
'SELECT * FROM users WHERE email = ?'
|
||||
).bind(email).first<User>();
|
||||
|
||||
if (!user) {
|
||||
const isNewAccount = !user;
|
||||
const needsEmailVerification = !user || !user.email_verified;
|
||||
|
||||
// If no user exists and no username provided, we need a username
|
||||
if (!user && !providedUsername) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'No verified CryptID account found for this email'
|
||||
error: 'No account found for this email. Please provide a CryptID username to create an account.',
|
||||
needsUsername: true
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this public key is already registered
|
||||
const existingKey = await db.prepare(
|
||||
'SELECT * FROM device_keys WHERE public_key = ?'
|
||||
).bind(publicKey).first<DeviceKey>();
|
||||
// Create user if doesn't exist
|
||||
if (!user && providedUsername) {
|
||||
const userId = generateUUID();
|
||||
await db.prepare(
|
||||
'INSERT INTO users (id, email, cryptid_username, email_verified) VALUES (?, ?, ?, 0)'
|
||||
).bind(userId, email, providedUsername).run();
|
||||
|
||||
if (existingKey) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Device already linked',
|
||||
cryptidUsername: user.cryptid_username,
|
||||
alreadyLinked: true
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
user = {
|
||||
id: userId,
|
||||
email,
|
||||
cryptid_username: providedUsername,
|
||||
email_verified: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
const userAgent = request.headers.get('User-Agent') || null;
|
||||
|
|
@ -329,37 +364,65 @@ export async function handleRequestDeviceLink(
|
|||
// Clean up old tokens
|
||||
await cleanupExpiredTokens(db);
|
||||
|
||||
// Create device link token (1 hour expiry for security)
|
||||
// Create combined device link + email verification token
|
||||
// Uses 'device_link' type but will also verify email when clicked
|
||||
const token = generateToken();
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
|
||||
|
||||
await db.prepare(
|
||||
'INSERT INTO verification_tokens (id, email, token, token_type, public_key, device_name, user_agent, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
).bind(generateUUID(), email, token, 'device_link', publicKey, deviceName || 'New Device', userAgent, expiresAt).run();
|
||||
|
||||
// Send device link email
|
||||
// Send combined verification email
|
||||
const linkUrl = `${env.APP_URL || 'https://jeffemmett.com'}/link-device?token=${token}`;
|
||||
const emailSent = await sendEmail(
|
||||
env,
|
||||
email,
|
||||
'Link new device to your CryptID',
|
||||
|
||||
// Different email content based on whether this is new account or existing
|
||||
const emailSubject = isNewAccount
|
||||
? 'Complete your CryptID setup'
|
||||
: needsEmailVerification
|
||||
? 'Verify email and link device to your CryptID'
|
||||
: 'Link new device to your CryptID';
|
||||
|
||||
const emailContent = isNewAccount
|
||||
? `
|
||||
<h2>Complete Your CryptID Setup</h2>
|
||||
<p>Click the link below to verify your email and set up your CryptID: <strong>${user!.cryptid_username}</strong></p>
|
||||
<p><strong>Device:</strong> ${deviceName || 'New Device'}</p>
|
||||
<p><a href="${linkUrl}" style="display: inline-block; padding: 12px 24px; background: #4f46e5; color: white; text-decoration: none; border-radius: 6px;">Complete Setup</a></p>
|
||||
<p>Or copy this link: ${linkUrl}</p>
|
||||
<p>This link expires in 1 hour.</p>
|
||||
<p style="color: #666; font-size: 12px;">If you didn't request this, you can safely ignore this email.</p>
|
||||
`
|
||||
: needsEmailVerification
|
||||
? `
|
||||
<h2>Verify Email & Link Device</h2>
|
||||
<p>Click the link below to verify your email and link this device to your CryptID: <strong>${user!.cryptid_username}</strong></p>
|
||||
<p><strong>Device:</strong> ${deviceName || 'New Device'}</p>
|
||||
<p><a href="${linkUrl}" style="display: inline-block; padding: 12px 24px; background: #4f46e5; color: white; text-decoration: none; border-radius: 6px;">Verify & Link Device</a></p>
|
||||
<p>Or copy this link: ${linkUrl}</p>
|
||||
<p>This link expires in 1 hour.</p>
|
||||
<p style="color: #c00; font-size: 12px;"><strong>If you didn't request this, do not click the link.</strong></p>
|
||||
`
|
||||
: `
|
||||
<h2>New Device Link Request</h2>
|
||||
<p>Someone is trying to link a new device to your CryptID: <strong>${user.cryptid_username}</strong></p>
|
||||
<p>Someone is trying to link a new device to your CryptID: <strong>${user!.cryptid_username}</strong></p>
|
||||
<p><strong>Device:</strong> ${deviceName || 'New Device'}</p>
|
||||
<p>If this was you, click the button below to approve:</p>
|
||||
<p><a href="${linkUrl}" style="display: inline-block; padding: 12px 24px; background: #4f46e5; color: white; text-decoration: none; border-radius: 6px;">Approve Device</a></p>
|
||||
<p>Or copy this link: ${linkUrl}</p>
|
||||
<p>This link expires in 1 hour.</p>
|
||||
<p style="color: #c00; font-size: 12px;"><strong>If you didn't request this, do not click the link.</strong> Someone may be trying to access your account.</p>
|
||||
`
|
||||
);
|
||||
`;
|
||||
|
||||
const emailSent = await sendEmail(env, email, emailSubject, emailContent);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: emailSent ? 'Verification email sent to your address' : 'Failed to send email',
|
||||
emailSent,
|
||||
cryptidUsername: user.cryptid_username
|
||||
cryptidUsername: user!.cryptid_username,
|
||||
isNewAccount,
|
||||
needsEmailVerification
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
|
@ -376,6 +439,10 @@ export async function handleRequestDeviceLink(
|
|||
/**
|
||||
* Complete device link (clicked from email on Device B)
|
||||
* GET /auth/link-device/:token
|
||||
*
|
||||
* Combined flow: This also verifies email if not already verified.
|
||||
* User clicks link from Device B, which links the device AND verifies the email
|
||||
* in one action - saving the user from needing to do two separate verifications.
|
||||
*/
|
||||
export async function handleLinkDevice(
|
||||
token: string,
|
||||
|
|
@ -414,6 +481,9 @@ export async function handleLinkDevice(
|
|||
});
|
||||
}
|
||||
|
||||
// Track if we're also verifying email
|
||||
const wasEmailUnverified = user.email_verified === 0;
|
||||
|
||||
// Add the new device key
|
||||
await db.prepare(
|
||||
'INSERT INTO device_keys (id, user_id, public_key, device_name, user_agent) VALUES (?, ?, ?, ?, ?)'
|
||||
|
|
@ -425,6 +495,14 @@ export async function handleLinkDevice(
|
|||
tokenRecord.user_agent
|
||||
).run();
|
||||
|
||||
// COMBINED FLOW: Also verify email if not already verified
|
||||
// This saves the user from needing to click a separate email verification link
|
||||
if (wasEmailUnverified) {
|
||||
await db.prepare(
|
||||
"UPDATE users SET email_verified = 1, updated_at = datetime('now') WHERE id = ?"
|
||||
).bind(user.id).run();
|
||||
}
|
||||
|
||||
// Mark token as used
|
||||
await db.prepare(
|
||||
'UPDATE verification_tokens SET used = 1 WHERE id = ?'
|
||||
|
|
@ -432,9 +510,13 @@ export async function handleLinkDevice(
|
|||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Device linked successfully',
|
||||
message: wasEmailUnverified
|
||||
? 'Device linked and email verified successfully'
|
||||
: 'Device linked successfully',
|
||||
cryptidUsername: user.cryptid_username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
emailVerified: true, // Now definitely verified
|
||||
emailWasJustVerified: wasEmailUnverified
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue