Other files
This commit is contained in:
@@ -1139,3 +1139,155 @@ def get_user_stores(user_id):
|
||||
'location': {'zip_code': user['zip_code'], 'radius_miles': user.get('radius_miles', 25)},
|
||||
'stores': result
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Checkout Profile Routes
|
||||
# =============================================================================
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile/data', methods=['GET'])
|
||||
def get_profile_data(user_id):
|
||||
"""
|
||||
Return non-sensitive profile fields for pre-filling the edit form.
|
||||
Only works if the profile is already unlocked (in memory).
|
||||
Card number is returned masked. Passwords are never returned.
|
||||
"""
|
||||
from src.profile_store import get_unlocked_profile, masked_card
|
||||
db = get_database()
|
||||
if not db.get_user(user_id):
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
profile = get_unlocked_profile(user_id)
|
||||
if not profile:
|
||||
return jsonify({'error': 'Profile not unlocked'}), 403
|
||||
|
||||
shipping = profile.get('shipping', {})
|
||||
creds = profile.get('site_credentials', {})
|
||||
card = masked_card(user_id)
|
||||
|
||||
return jsonify({
|
||||
'shipping': shipping,
|
||||
'masked_card': card,
|
||||
'site_credentials': {
|
||||
'target_email': creds.get('target_email', ''),
|
||||
'bestbuy_email': creds.get('bestbuy_email', ''),
|
||||
'gamestop_email': creds.get('gamestop_email', ''),
|
||||
# passwords intentionally omitted
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile', methods=['GET'])
|
||||
def get_profile_status(user_id):
|
||||
"""
|
||||
Return profile metadata — never returns card data or decrypted fields.
|
||||
Frontend uses this to show lock/unlock state and masked card info.
|
||||
"""
|
||||
from src.profile_store import is_unlocked, masked_card
|
||||
db = get_database()
|
||||
if not db.get_user(user_id):
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
has_profile = db.user_has_profile(user_id)
|
||||
unlocked = is_unlocked(user_id)
|
||||
|
||||
return jsonify({
|
||||
'has_profile': has_profile,
|
||||
'is_unlocked': unlocked,
|
||||
'masked_card': masked_card(user_id) if unlocked else None,
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile', methods=['POST'])
|
||||
def save_profile(user_id):
|
||||
"""
|
||||
Save (or replace) an encrypted checkout profile.
|
||||
Expects JSON: { password, shipping: {...}, payment: {...} }
|
||||
Card data is encrypted immediately — never stored in plaintext.
|
||||
"""
|
||||
from src.profile_store import save_profile as _save
|
||||
db = get_database()
|
||||
if not db.get_user(user_id):
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No data provided'}), 400
|
||||
|
||||
password = data.get('password', '').strip()
|
||||
if len(password) < 8:
|
||||
return jsonify({'error': 'Password must be at least 8 characters'}), 400
|
||||
|
||||
shipping = data.get('shipping', {})
|
||||
payment = data.get('payment', {})
|
||||
site_credentials = data.get('site_credentials', {})
|
||||
|
||||
required_shipping = ['first_name', 'last_name', 'address1', 'city', 'state', 'zip', 'email']
|
||||
missing = [f for f in required_shipping if not shipping.get(f)]
|
||||
if missing:
|
||||
return jsonify({'error': f'Missing shipping fields: {", ".join(missing)}'}), 400
|
||||
|
||||
# If card fields are blank and a profile already exists, keep the existing card data
|
||||
if not payment.get('card_number') and db.user_has_profile(user_id):
|
||||
from src.profile_store import get_unlocked_profile
|
||||
existing = get_unlocked_profile(user_id)
|
||||
if existing:
|
||||
for k in ['card_number', 'expiry_month', 'expiry_year', 'cvv', 'name_on_card']:
|
||||
if not payment.get(k):
|
||||
payment[k] = existing.get('payment', {}).get(k, '')
|
||||
|
||||
required_payment = ['card_number', 'expiry_month', 'expiry_year', 'cvv']
|
||||
missing = [f for f in required_payment if not payment.get(f)]
|
||||
if missing:
|
||||
return jsonify({'error': f'Missing payment fields: {", ".join(missing)}'}), 400
|
||||
|
||||
try:
|
||||
ok = _save(user_id, password, shipping, payment, db, site_credentials=site_credentials)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if not ok:
|
||||
return jsonify({'error': 'Encryption failed — is the cryptography package installed? Run: pip install cryptography'}), 500
|
||||
|
||||
return jsonify({'success': True, 'is_unlocked': True})
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile/unlock', methods=['POST'])
|
||||
def unlock_profile(user_id):
|
||||
"""Decrypt profile into memory. Expects JSON: { password }"""
|
||||
from src.profile_store import unlock_profile as _unlock
|
||||
db = get_database()
|
||||
if not db.get_user(user_id):
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
if not db.user_has_profile(user_id):
|
||||
return jsonify({'error': 'No profile saved for this user'}), 404
|
||||
|
||||
data = request.get_json()
|
||||
password = (data or {}).get('password', '')
|
||||
|
||||
if not _unlock(user_id, password, db):
|
||||
return jsonify({'error': 'Incorrect password'}), 401
|
||||
|
||||
from src.profile_store import masked_card
|
||||
return jsonify({'success': True, 'masked_card': masked_card(user_id)})
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile/lock', methods=['POST'])
|
||||
def lock_profile(user_id):
|
||||
"""Clear profile from memory."""
|
||||
from src.profile_store import lock_profile as _lock
|
||||
_lock(user_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>/profile', methods=['DELETE'])
|
||||
def delete_profile(user_id):
|
||||
"""Permanently delete a user's saved profile."""
|
||||
from src.profile_store import lock_profile as _lock
|
||||
db = get_database()
|
||||
if not db.get_user(user_id):
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
_lock(user_id)
|
||||
db.delete_user_profile(user_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
+193
-7
@@ -1219,10 +1219,10 @@ async function loadUsers() {
|
||||
const data = await api('/users');
|
||||
if (!data) return;
|
||||
|
||||
renderUsersList(data.users || []);
|
||||
await renderUsersList(data.users || []);
|
||||
}
|
||||
|
||||
function renderUsersList(users) {
|
||||
async function renderUsersList(users) {
|
||||
const container = document.getElementById('usersList');
|
||||
if (!container) return;
|
||||
|
||||
@@ -1231,24 +1231,210 @@ function renderUsersList(users) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = users.map(user => `
|
||||
// Fetch profile status for all users in parallel
|
||||
const profileStatuses = await Promise.all(
|
||||
users.map(u => api(`/users/${u.id}/profile`).catch(() => null))
|
||||
);
|
||||
|
||||
container.innerHTML = users.map((user, i) => {
|
||||
const ps = profileStatuses[i] || {};
|
||||
const hasProfile = ps.has_profile;
|
||||
const isUnlocked = ps.is_unlocked;
|
||||
const maskedCard = ps.masked_card || '';
|
||||
|
||||
const profileBadge = isUnlocked
|
||||
? `<span class="badge badge-success">🔓 Unlocked ${maskedCard ? '· ' + escapeHtml(maskedCard) : ''}</span>`
|
||||
: hasProfile
|
||||
? `<span class="badge badge-warning">🔒 Locked</span>`
|
||||
: `<span class="badge badge-muted">No profile</span>`;
|
||||
|
||||
const profileBtns = hasProfile
|
||||
? isUnlocked
|
||||
? `<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
|
||||
<button class="btn-secondary btn-small" onclick="lockProfile(${user.id})">Lock</button>
|
||||
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
|
||||
: `<button class="btn-primary btn-small" onclick="openUnlockModal(${user.id}, '${escapeHtml(user.name)}')">Unlock</button>
|
||||
<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
|
||||
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
|
||||
: `<button class="btn-primary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Set Up Profile</button>`;
|
||||
|
||||
return `
|
||||
<div class="user-card" data-user-id="${user.id}">
|
||||
<div class="user-header">
|
||||
<span class="user-name">${escapeHtml(user.name)}</span>
|
||||
<span class="user-location">${user.zip_code ? `${user.zip_code} (${user.radius_miles}mi)` : 'No location set'}</span>
|
||||
${profileBadge}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="form-inline">
|
||||
<input type="text" class="user-zip-input" placeholder="Zip Code" value="${user.zip_code || ''}" maxlength="10">
|
||||
<input type="number" class="user-radius-input" placeholder="Radius" value="${user.radius_miles || 25}" min="5" max="100" style="width: 80px;">
|
||||
<button class="btn-secondary btn-small" onclick="updateUserLocation(${user.id})">Update</button>
|
||||
<button class="btn-secondary btn-small" onclick="updateUserLocation(${user.id})">Update Location</button>
|
||||
<button class="btn-secondary btn-small" onclick="viewUserStores(${user.id})">View Stores</button>
|
||||
<button class="btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
|
||||
<button class="btn-danger btn-small" onclick="deleteUser(${user.id})">Delete User</button>
|
||||
</div>
|
||||
<div class="form-inline" style="margin-top:.5rem;">
|
||||
${profileBtns}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-stores" id="userStores-${user.id}" style="display: none;"></div>
|
||||
</div>
|
||||
`).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ---- Profile modal ----
|
||||
let _profileUserId = null;
|
||||
|
||||
async function openProfileModal(userId, userName) {
|
||||
_profileUserId = userId;
|
||||
document.getElementById('profileModalTitle').textContent = `Checkout Profile — ${userName}`;
|
||||
document.getElementById('profileModalError').style.display = 'none';
|
||||
|
||||
// Clear all fields first
|
||||
['pf-first-name','pf-last-name','pf-address1','pf-address2','pf-city','pf-state',
|
||||
'pf-zip','pf-phone','pf-email','pf-card-number','pf-expiry-month','pf-expiry-year',
|
||||
'pf-cvv','pf-card-name','pf-target-email','pf-target-password',
|
||||
'pf-bestbuy-email','pf-bestbuy-password','pf-gamestop-email','pf-gamestop-password',
|
||||
'pf-password','pf-password-confirm'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
|
||||
// Pre-fill from unlocked profile if available
|
||||
const data = await api(`/users/${userId}/profile/data`).catch(() => null);
|
||||
if (data && data.shipping) {
|
||||
const s = data.shipping;
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el && val) el.value = val; };
|
||||
set('pf-first-name', s.first_name);
|
||||
set('pf-last-name', s.last_name);
|
||||
set('pf-address1', s.address1);
|
||||
set('pf-address2', s.address2);
|
||||
set('pf-city', s.city);
|
||||
set('pf-state', s.state);
|
||||
set('pf-zip', s.zip);
|
||||
set('pf-phone', s.phone);
|
||||
set('pf-email', s.email);
|
||||
set('pf-target-email', data.site_credentials?.target_email);
|
||||
set('pf-bestbuy-email', data.site_credentials?.bestbuy_email);
|
||||
set('pf-gamestop-email', data.site_credentials?.gamestop_email);
|
||||
// Show masked card as placeholder so user knows card is saved
|
||||
if (data.masked_card) {
|
||||
const cardEl = document.getElementById('pf-card-number');
|
||||
if (cardEl) cardEl.placeholder = `${data.masked_card} (leave blank to keep)`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('profileModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeProfileModal() {
|
||||
document.getElementById('profileModal').style.display = 'none';
|
||||
_profileUserId = null;
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const errEl = document.getElementById('profileModalError');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const password = document.getElementById('pf-password').value;
|
||||
const confirm = document.getElementById('pf-password-confirm').value;
|
||||
|
||||
if (password.length < 8) {
|
||||
errEl.textContent = 'Password must be at least 8 characters.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
if (password !== confirm) {
|
||||
errEl.textContent = 'Passwords do not match.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const shipping = {
|
||||
first_name: document.getElementById('pf-first-name').value.trim(),
|
||||
last_name: document.getElementById('pf-last-name').value.trim(),
|
||||
address1: document.getElementById('pf-address1').value.trim(),
|
||||
address2: document.getElementById('pf-address2').value.trim(),
|
||||
city: document.getElementById('pf-city').value.trim(),
|
||||
state: document.getElementById('pf-state').value.trim().toUpperCase(),
|
||||
zip: document.getElementById('pf-zip').value.trim(),
|
||||
phone: document.getElementById('pf-phone').value.trim(),
|
||||
email: document.getElementById('pf-email').value.trim(),
|
||||
};
|
||||
const payment = {
|
||||
card_number: document.getElementById('pf-card-number').value.replace(/\s/g, ''),
|
||||
expiry_month: document.getElementById('pf-expiry-month').value.trim(),
|
||||
expiry_year: document.getElementById('pf-expiry-year').value.trim(),
|
||||
cvv: document.getElementById('pf-cvv').value.trim(),
|
||||
name_on_card: document.getElementById('pf-card-name').value.trim(),
|
||||
};
|
||||
const site_credentials = {
|
||||
target_email: document.getElementById('pf-target-email')?.value.trim(),
|
||||
target_password: document.getElementById('pf-target-password')?.value,
|
||||
bestbuy_email: document.getElementById('pf-bestbuy-email')?.value.trim(),
|
||||
bestbuy_password: document.getElementById('pf-bestbuy-password')?.value,
|
||||
gamestop_email: document.getElementById('pf-gamestop-email')?.value.trim(),
|
||||
gamestop_password: document.getElementById('pf-gamestop-password')?.value,
|
||||
};
|
||||
|
||||
const result = await api(`/users/${_profileUserId}/profile`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password, shipping, payment, site_credentials })
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
closeProfileModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
errEl.textContent = result?.error || 'Failed to save profile.';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Unlock modal ----
|
||||
let _unlockUserId = null;
|
||||
|
||||
function openUnlockModal(userId, userName) {
|
||||
_unlockUserId = userId;
|
||||
document.getElementById('unlockModalTitle').textContent = `Unlock Profile — ${userName}`;
|
||||
document.getElementById('unlock-password').value = '';
|
||||
document.getElementById('unlockModalError').style.display = 'none';
|
||||
document.getElementById('unlockModal').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('unlock-password').focus(), 100);
|
||||
}
|
||||
|
||||
function closeUnlockModal() {
|
||||
document.getElementById('unlockModal').style.display = 'none';
|
||||
_unlockUserId = null;
|
||||
}
|
||||
|
||||
async function submitUnlock() {
|
||||
const errEl = document.getElementById('unlockModalError');
|
||||
const password = document.getElementById('unlock-password').value;
|
||||
|
||||
const result = await api(`/users/${_unlockUserId}/profile/unlock`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
closeUnlockModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
errEl.textContent = result?.error || 'Incorrect password.';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function lockProfile(userId) {
|
||||
await api(`/users/${userId}/profile/lock`, { method: 'POST' });
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
async function deleteProfile(userId) {
|
||||
if (!confirm('Delete this checkout profile? This cannot be undone.')) return;
|
||||
await api(`/users/${userId}/profile`, { method: 'DELETE' });
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
async function addUser() {
|
||||
|
||||
@@ -922,6 +922,9 @@ h3 {
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
@@ -931,25 +934,84 @@ h3 {
|
||||
.modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-body h4 {
|
||||
margin: 1.2rem 0 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-body h4:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 20px;
|
||||
font-size: 24px;
|
||||
top: 16px;
|
||||
right: 18px;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
text-align: center;
|
||||
|
||||
@@ -337,14 +337,14 @@
|
||||
<!-- Users Page -->
|
||||
<section id="page-users" class="page">
|
||||
<h1>Users</h1>
|
||||
<p class="help-text">Manage users and their store locations for local stock checking</p>
|
||||
<p class="help-text">Manage users, store locations, and encrypted checkout profiles</p>
|
||||
|
||||
<div class="users-section">
|
||||
<div class="add-user-form">
|
||||
<h3>Add New User</h3>
|
||||
<div class="form-group">
|
||||
<label for="newUserName">Name</label>
|
||||
<input type="text" id="newUserName" placeholder="e.g., John">
|
||||
<input type="text" id="newUserName" placeholder="e.g., Michael">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserZip">Zip Code</label>
|
||||
@@ -363,6 +363,94 @@
|
||||
<p class="loading">Loading users...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkout Profile Modal -->
|
||||
<div id="profileModal" class="modal" style="display:none;">
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3 id="profileModalTitle">Checkout Profile</h3>
|
||||
<button class="modal-close" onclick="closeProfileModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="help-text" style="margin-bottom:1rem;">
|
||||
Your card and shipping data is encrypted with your password before saving.
|
||||
It is never stored in plaintext and is never sent back to the browser.
|
||||
</p>
|
||||
<h4>Shipping Address</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>First Name</label><input type="text" id="pf-first-name" placeholder="Michael"></div>
|
||||
<div class="form-group"><label>Last Name</label><input type="text" id="pf-last-name" placeholder="Smith"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Address</label><input type="text" id="pf-address1" placeholder="123 Main St"></div>
|
||||
<div class="form-group"><label>Address Line 2 (optional)</label><input type="text" id="pf-address2" placeholder="Apt 4B"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>City</label><input type="text" id="pf-city" placeholder="Dallas"></div>
|
||||
<div class="form-group" style="max-width:80px;"><label>State</label><input type="text" id="pf-state" placeholder="TX" maxlength="2"></div>
|
||||
<div class="form-group" style="max-width:120px;"><label>Zip</label><input type="text" id="pf-zip" placeholder="75201" maxlength="10"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Phone</label><input type="tel" id="pf-phone" placeholder="214-555-0100"></div>
|
||||
<div class="form-group"><label>Email</label><input type="email" id="pf-email" placeholder="you@email.com"></div>
|
||||
</div>
|
||||
<h4 style="margin-top:1.5rem;">Payment</h4>
|
||||
<div class="form-group"><label>Card Number</label><input type="text" id="pf-card-number" placeholder="4111 1111 1111 1111" maxlength="19" autocomplete="cc-number"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="max-width:100px;"><label>Exp Month</label><input type="text" id="pf-expiry-month" placeholder="01" maxlength="2"></div>
|
||||
<div class="form-group" style="max-width:110px;"><label>Exp Year</label><input type="text" id="pf-expiry-year" placeholder="2027" maxlength="4"></div>
|
||||
<div class="form-group" style="max-width:90px;"><label>CVV</label><input type="password" id="pf-cvv" placeholder="•••" maxlength="4" autocomplete="cc-csc"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Name on Card</label><input type="text" id="pf-card-name" placeholder="Michael Smith" autocomplete="cc-name"></div>
|
||||
<h4 style="margin-top:1.5rem;">Site Logins (optional but recommended)</h4>
|
||||
<p class="help-text">Used to log in automatically at checkout. Leave blank to check out as guest.</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Target Email</label><input type="email" id="pf-target-email" placeholder="you@email.com"></div>
|
||||
<div class="form-group"><label>Target Password</label><input type="password" id="pf-target-password" placeholder="••••••••"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Best Buy Email</label><input type="email" id="pf-bestbuy-email" placeholder="you@email.com"></div>
|
||||
<div class="form-group"><label>Best Buy Password</label><input type="password" id="pf-bestbuy-password" placeholder="••••••••"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>GameStop Email</label><input type="email" id="pf-gamestop-email" placeholder="you@email.com"></div>
|
||||
<div class="form-group"><label>GameStop Password</label><input type="password" id="pf-gamestop-password" placeholder="••••••••"></div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top:1.5rem;">Profile Password</h4>
|
||||
<p class="help-text">Encrypts your profile. You'll need this to unlock before each buy session.</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Password (min 8 chars)</label><input type="password" id="pf-password" placeholder="••••••••••••"></div>
|
||||
<div class="form-group"><label>Confirm Password</label><input type="password" id="pf-password-confirm" placeholder="••••••••••••"></div>
|
||||
</div>
|
||||
<div id="profileModalError" class="error-message" style="display:none; color:#e74c3c; margin-top:.5rem;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeProfileModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="saveProfile()">Encrypt & Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unlock Modal -->
|
||||
<div id="unlockModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="unlockModalTitle">Unlock Profile</h3>
|
||||
<button class="modal-close" onclick="closeUnlockModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="help-text">Enter your password to load your checkout data into memory for this session.</p>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="unlock-password" placeholder="••••••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<div id="unlockModalError" class="error-message" style="display:none; color:#e74c3c; margin-top:.5rem;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeUnlockModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="submitUnlock()">Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings Page -->
|
||||
|
||||
Reference in New Issue
Block a user