We sent a verification link to:
+{email}
+ {pendingCryptId && ( ++ This will link this device to CryptID: {pendingCryptId} +
+ )} ++ Click the link in the email to complete sign in on this device. + The link expires in 1 hour. +
+ ++ Enter the email address linked to your CryptID account. + We'll send a verification link to complete sign in on this device. +
+
{isRegistering
@@ -239,9 +413,9 @@ const CryptID: React.FC
Check your inbox for the verification email.
+ )} + {emailStatus.verified && ( ++ You can now sign in on other devices using this email. +
+ )} ++ Link an email to access your CryptID from multiple devices. +
+{emailError}
} + {emailSuccess &&{emailSuccess}
} +Loading devices...
+ ) : devices.length === 0 ? ( +No devices linked yet.
+ ) : ( +Remember to back up your encryption keys to prevent data loss!
diff --git a/src/css/crypto-auth.css b/src/css/crypto-auth.css index 7fa017b..485f825 100644 --- a/src/css/crypto-auth.css +++ b/src/css/crypto-auth.css @@ -642,4 +642,546 @@ html.dark .debug-input { html.dark .debug-results h3 { color: #e2e8f0; - } \ No newline at end of file + } + +/* ============================================= + Email Sign-in & Device Linking Styles + ============================================= */ + +/* Email sign-in section divider */ +.email-signin-section { + margin-top: 1.5rem; + padding-top: 1rem; +} + +.email-signin-section .divider { + display: flex; + align-items: center; + margin-bottom: 1rem; + color: #6c757d; + font-size: 0.85rem; +} + +.email-signin-section .divider::before, +.email-signin-section .divider::after { + content: ''; + flex: 1; + border-bottom: 1px solid #e9ecef; +} + +.email-signin-section .divider span { + padding: 0 0.75rem; +} + +.email-signin-button { + width: 100%; + padding: 0.75rem; + background: #6c757d; + color: white; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.email-signin-button:hover:not(:disabled) { + background: #5a6268; +} + +.email-signin-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.email-signin-hint { + margin-top: 0.5rem; + font-size: 0.8rem; + color: #6c757d; + text-align: center; +} + +/* Email link pending state */ +.email-link-pending { + text-align: center; + padding: 1rem; +} + +.email-link-pending .email-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.email-link-pending .email-address { + font-size: 1.1rem; + margin: 0.5rem 0 1rem; +} + +.email-link-pending .cryptid-info { + background: #e3f2fd; + padding: 0.75rem; + border-radius: 6px; + margin: 1rem 0; + font-size: 0.9rem; +} + +.email-link-pending .email-instructions { + color: #6c757d; + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +/* Profile Email Settings */ +.email-settings { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #e9ecef; +} + +.email-status-section { + margin-bottom: 1rem; +} + +.email-linked .email-info { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.email-linked .email-label { + color: #6c757d; + font-size: 0.9rem; +} + +.email-linked .email-value { + font-weight: 500; + color: #495057; +} + +.email-verified-badge { + background: #d4edda; + color: #155724; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; +} + +.email-pending-badge { + background: #fff3cd; + color: #856404; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; +} + +.email-hint { + font-size: 0.85rem; + color: #6c757d; + margin: 0.5rem 0; +} + +.email-description { + font-size: 0.9rem; + color: #6c757d; + margin-bottom: 1rem; +} + +.link-email-button { + padding: 0.6rem 1rem; + background: #007bff; + color: white; + border: none; + border-radius: 6px; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.link-email-button:hover { + background: #0056b3; +} + +/* Email form */ +.email-form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.email-input { + padding: 0.6rem; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 0.95rem; +} + +.email-input:focus { + outline: none; + border-color: #007bff; +} + +.email-form-actions { + display: flex; + gap: 0.5rem; +} + +.email-error { + color: #dc3545; + font-size: 0.85rem; + margin-top: 0.5rem; +} + +.email-success { + color: #28a745; + font-size: 0.85rem; + margin-top: 0.5rem; +} + +/* Devices Section */ +.devices-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e9ecef; +} + +.toggle-devices-button { + background: none; + border: none; + color: #007bff; + font-size: 0.9rem; + cursor: pointer; + text-decoration: underline; + padding: 0; +} + +.toggle-devices-button:hover { + color: #0056b3; +} + +.devices-list { + margin-top: 1rem; +} + +.devices-loading, +.no-devices { + color: #6c757d; + font-size: 0.9rem; + font-style: italic; +} + +.device-list { + list-style: none; + padding: 0; + margin: 0; +} + +.device-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: #f8f9fa; + border-radius: 6px; + margin-bottom: 0.5rem; + border: 1px solid #e9ecef; +} + +.device-item.current-device { + border-color: #007bff; + background: #e7f3ff; +} + +.device-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.device-name { + font-weight: 500; + color: #495057; + font-size: 0.95rem; +} + +.device-name .current-badge { + color: #007bff; + font-weight: normal; + font-size: 0.85rem; +} + +.device-meta { + font-size: 0.8rem; + color: #6c757d; +} + +.revoke-device-button { + padding: 0.4rem 0.75rem; + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + font-size: 0.8rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.revoke-device-button:hover { + background: #c82333; +} + +/* Verification Pages */ +.verify-email-page, +.link-device-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #f8f9fa; + padding: 1rem; +} + +.verify-email-container, +.link-device-container { + max-width: 400px; + width: 100%; + padding: 2rem; + background: white; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.loading-spinner { + width: 48px; + height: 48px; + border: 4px solid #e9ecef; + border-top-color: #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1.5rem; +} + +.success-icon { + width: 64px; + height: 64px; + border-radius: 50%; + background: #28a745; + color: white; + font-size: 2rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.5rem; +} + +.error-icon { + width: 64px; + height: 64px; + border-radius: 50%; + background: #dc3545; + color: white; + font-size: 2rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.5rem; +} + +.verify-email-container h2, +.link-device-container h2 { + margin: 0 0 1rem; + color: #1a1a1a; + font-size: 1.5rem; +} + +.verified-email, +.cryptid-username { + background: #e3f2fd; + padding: 0.75rem; + border-radius: 6px; + margin: 1rem 0; + font-size: 0.95rem; +} + +.redirect-notice { + color: #6c757d; + font-size: 0.9rem; + margin: 1rem 0; +} + +.error-hint { + color: #6c757d; + font-size: 0.85rem; + margin: 1rem 0; + padding: 0.75rem; + background: #f8f9fa; + border-radius: 6px; +} + +.continue-button, +.retry-button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.continue-button { + background: #007bff; + color: white; +} + +.continue-button:hover { + background: #0056b3; +} + +.retry-button { + background: #6c757d; + color: white; +} + +.retry-button:hover { + background: #5a6268; +} + +/* Dark mode for email/device styles */ +html.dark .email-signin-section .divider { + color: #a0aec0; +} + +html.dark .email-signin-section .divider::before, +html.dark .email-signin-section .divider::after { + border-bottom-color: #4a5568; +} + +html.dark .email-signin-button { + background: #4a5568; +} + +html.dark .email-signin-button:hover:not(:disabled) { + background: #718096; +} + +html.dark .email-signin-hint { + color: #a0aec0; +} + +html.dark .email-link-pending .cryptid-info { + background: #2c5282; + color: #e2e8f0; +} + +html.dark .email-settings { + border-top-color: #4a5568; +} + +html.dark .email-linked .email-label { + color: #a0aec0; +} + +html.dark .email-linked .email-value { + color: #e2e8f0; +} + +html.dark .email-verified-badge { + background: #276749; + color: #9ae6b4; +} + +html.dark .email-pending-badge { + background: #744210; + color: #faf089; +} + +html.dark .email-hint, +html.dark .email-description { + color: #a0aec0; +} + +html.dark .email-input { + background: #2d3748; + border-color: #4a5568; + color: #f7fafc; +} + +html.dark .email-input:focus { + border-color: #63b3ed; +} + +html.dark .devices-section { + border-top-color: #4a5568; +} + +html.dark .toggle-devices-button { + color: #63b3ed; +} + +html.dark .toggle-devices-button:hover { + color: #90cdf4; +} + +html.dark .devices-loading, +html.dark .no-devices { + color: #a0aec0; +} + +html.dark .device-item { + background: #4a5568; + border-color: #718096; +} + +html.dark .device-item.current-device { + border-color: #63b3ed; + background: #2c5282; +} + +html.dark .device-name { + color: #e2e8f0; +} + +html.dark .device-name .current-badge { + color: #90cdf4; +} + +html.dark .device-meta { + color: #a0aec0; +} + +html.dark .verify-email-page, +html.dark .link-device-page { + background: #1a202c; +} + +html.dark .verify-email-container, +html.dark .link-device-container { + background: #2d3748; + border: 1px solid #4a5568; +} + +html.dark .verify-email-container h2, +html.dark .link-device-container h2 { + color: #f7fafc; +} + +html.dark .verified-email, +html.dark .cryptid-username { + background: #2c5282; + color: #e2e8f0; +} + +html.dark .redirect-notice { + color: #a0aec0; +} + +html.dark .error-hint { + background: #4a5568; + color: #a0aec0; +} \ No newline at end of file diff --git a/worker/types.ts b/worker/types.ts index 2abb101..bfedfb1 100644 --- a/worker/types.ts +++ b/worker/types.ts @@ -6,21 +6,20 @@ export interface Environment { TLDRAW_BUCKET: R2Bucket BOARD_BACKUPS_BUCKET: R2Bucket AUTOMERGE_DURABLE_OBJECT: DurableObjectNamespace + CRYPTID_DB: D1Database DAILY_API_KEY: string; DAILY_DOMAIN: string; - // CryptID auth bindings - CRYPTID_DB?: D1Database; - SENDGRID_API_KEY?: string; - CRYPTID_EMAIL_FROM?: string; - APP_URL?: string; + SENDGRID_API_KEY: string; + CRYPTID_EMAIL_FROM: string; + APP_URL: string; } -// CryptID types for auth +// CryptID Database Types export interface User { id: string; cryptid_username: string; email: string | null; - email_verified: boolean; + email_verified: number; created_at: string; updated_at: string; } @@ -37,15 +36,13 @@ export interface DeviceKey { export interface VerificationToken { id: string; - user_id: string; + email: string; token: string; - type: 'email_verification' | 'device_link'; + token_type: 'email_verify' | 'device_link'; + public_key: string | null; + device_name: string | null; + user_agent: string | null; expires_at: string; + used: number; created_at: string; - metadata: string | null; - // Metadata fields that get parsed from JSON - email?: string; - public_key?: string; - device_name?: string; - user_agent?: string; -} \ No newline at end of file +} diff --git a/worker/worker.ts b/worker/worker.ts index 0a5ed77..8b5fa5a 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -1,6 +1,15 @@ import { AutoRouter, cors, error, IRequest } from "itty-router" import { handleAssetDownload, handleAssetUpload } from "./assetUploads" import { Environment } from "./types" +import { + handleLinkEmail, + handleVerifyEmail, + handleRequestDeviceLink, + handleLinkDevice, + handleLookup, + handleGetDevices, + handleRevokeDevice +} from "./cryptidAuth" // make sure our sync durable objects are made available to cloudflare export { AutomergeDurableObject } from "./AutomergeDurableObject" @@ -800,6 +809,25 @@ const router = AutoRouter