fix: namespace crypto storage per account, add chat polling, fix notification flow and dark theme issues
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
--color-accent-text: #fff;
|
--color-accent-text: #fff;
|
||||||
|
|
||||||
--color-success: #16A34A;
|
--color-success: #16A34A;
|
||||||
|
--color-success-text: #fff;
|
||||||
--color-warning: #D97706;
|
--color-warning: #D97706;
|
||||||
--color-error: #DC2626;
|
--color-error: #DC2626;
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@
|
|||||||
--color-accent-text: #042F2E;
|
--color-accent-text: #042F2E;
|
||||||
|
|
||||||
--color-success: #4ADE80;
|
--color-success: #4ADE80;
|
||||||
|
--color-success-text: #042F2E;
|
||||||
--color-warning: #FBBF24;
|
--color-warning: #FBBF24;
|
||||||
--color-error: #F87171;
|
--color-error: #F87171;
|
||||||
|
|
||||||
@@ -142,6 +144,7 @@
|
|||||||
--color-accent-text: #042F2E;
|
--color-accent-text: #042F2E;
|
||||||
|
|
||||||
--color-success: #4ADE80;
|
--color-success: #4ADE80;
|
||||||
|
--color-success-text: #042F2E;
|
||||||
--color-warning: #FBBF24;
|
--color-warning: #FBBF24;
|
||||||
--color-error: #F87171;
|
--color-error: #F87171;
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ if (!$type) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only handle settled invoices
|
// Only handle fully settled invoices (not partial payment events)
|
||||||
if ($type !== 'InvoiceSettled' && $type !== 'InvoicePaymentSettled') {
|
if ($type !== 'InvoiceSettled') {
|
||||||
echo json_encode(['ok' => true, 'action' => 'ignored', 'type' => $type]);
|
echo json_encode(['ok' => true, 'action' => 'ignored', 'type' => $type]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -125,29 +125,49 @@ curl_close($gch);
|
|||||||
$listingData = json_decode($getResponse, true);
|
$listingData = json_decode($getResponse, true);
|
||||||
$userCreated = $listingData['data']['user_created'] ?? null;
|
$userCreated = $listingData['data']['user_created'] ?? null;
|
||||||
|
|
||||||
// Create notification for listing owner
|
// Create notification for listing owner (skip if already exists)
|
||||||
if ($userCreated) {
|
if ($userCreated) {
|
||||||
$notifPayload = json_encode([
|
$checkUrl = DIRECTUS_URL . '/items/notifications?' . http_build_query([
|
||||||
'user_hash' => $userCreated,
|
'filter' => json_encode([
|
||||||
'type' => 'listing_published',
|
'user_hash' => ['_eq' => $userCreated],
|
||||||
'reference_id' => $listingId,
|
'type' => ['_eq' => 'listing_published'],
|
||||||
'read' => false,
|
'reference_id' => ['_eq' => $listingId],
|
||||||
|
]),
|
||||||
|
'limit' => 1,
|
||||||
]);
|
]);
|
||||||
|
$ech = curl_init($checkUrl);
|
||||||
$notifUrl = DIRECTUS_URL . '/items/notifications';
|
curl_setopt_array($ech, [
|
||||||
$nch = curl_init($notifUrl);
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . DIRECTUS_TOKEN],
|
||||||
curl_setopt_array($nch, [
|
|
||||||
CURLOPT_POST => true,
|
|
||||||
CURLOPT_POSTFIELDS => $notifPayload,
|
|
||||||
CURLOPT_HTTPHEADER => [
|
|
||||||
'Content-Type: application/json',
|
|
||||||
'Authorization: Bearer ' . DIRECTUS_TOKEN,
|
|
||||||
],
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 5,
|
CURLOPT_TIMEOUT => 5,
|
||||||
]);
|
]);
|
||||||
curl_exec($nch);
|
$existingResp = curl_exec($ech);
|
||||||
curl_close($nch);
|
curl_close($ech);
|
||||||
|
$existingData = json_decode($existingResp, true);
|
||||||
|
|
||||||
|
if (empty($existingData['data'])) {
|
||||||
|
$notifPayload = json_encode([
|
||||||
|
'user_hash' => $userCreated,
|
||||||
|
'type' => 'listing_published',
|
||||||
|
'reference_id' => $listingId,
|
||||||
|
'read' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$notifUrl = DIRECTUS_URL . '/items/notifications';
|
||||||
|
$nch = curl_init($notifUrl);
|
||||||
|
curl_setopt_array($nch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $notifPayload,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . DIRECTUS_TOKEN,
|
||||||
|
],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
curl_exec($nch);
|
||||||
|
curl_close($nch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['ok' => true, 'listingId' => $listingId, 'action' => 'published']);
|
echo json_encode(['ok' => true, 'listingId' => $listingId, 'action' => 'published']);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
--color-primary: #525252; --color-primary-hover: #3F3F3F; --color-primary-light: #E7E5E4;
|
--color-primary: #525252; --color-primary-hover: #3F3F3F; --color-primary-light: #E7E5E4;
|
||||||
--color-secondary: #737373; --color-secondary-hover: #525252;
|
--color-secondary: #737373; --color-secondary-hover: #525252;
|
||||||
--color-accent: #0D9488; --color-accent-hover: #0F766E; --color-accent-text: #fff;
|
--color-accent: #0D9488; --color-accent-hover: #0F766E; --color-accent-text: #fff;
|
||||||
--color-success: #16A34A; --color-warning: #D97706; --color-error: #DC2626;
|
--color-success: #16A34A; --color-success-text: #fff; --color-warning: #D97706; --color-error: #DC2626;
|
||||||
--color-bg: #FAFAF9; --color-bg-secondary: #F5F5F4; --color-bg-tertiary: #E7E5E4;
|
--color-bg: #FAFAF9; --color-bg-secondary: #F5F5F4; --color-bg-tertiary: #E7E5E4;
|
||||||
--color-text: #1C1917; --color-text-secondary: #44403C; --color-text-muted: #78716C;
|
--color-text: #1C1917; --color-text-secondary: #44403C; --color-text-muted: #78716C;
|
||||||
--color-border: #D6D3D1; --color-border-focus: #0D9488;
|
--color-border: #D6D3D1; --color-border-focus: #0D9488;
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
--color-primary: #A8A29E; --color-primary-hover: #D6D3D1; --color-primary-light: #292524;
|
--color-primary: #A8A29E; --color-primary-hover: #D6D3D1; --color-primary-light: #292524;
|
||||||
--color-secondary: #A8A29E; --color-secondary-hover: #D6D3D1;
|
--color-secondary: #A8A29E; --color-secondary-hover: #D6D3D1;
|
||||||
--color-accent: #2DD4BF; --color-accent-hover: #5EEAD4; --color-accent-text: #042F2E;
|
--color-accent: #2DD4BF; --color-accent-hover: #5EEAD4; --color-accent-text: #042F2E;
|
||||||
--color-success: #4ADE80; --color-warning: #FBBF24; --color-error: #F87171;
|
--color-success: #4ADE80; --color-success-text: #042F2E; --color-warning: #FBBF24; --color-error: #F87171;
|
||||||
--color-bg: #171717; --color-bg-secondary: #1C1917; --color-bg-tertiary: #292524;
|
--color-bg: #171717; --color-bg-secondary: #1C1917; --color-bg-tertiary: #292524;
|
||||||
--color-text: #F5F5F4; --color-text-secondary: #D6D3D1; --color-text-muted: #A8A29E;
|
--color-text: #F5F5F4; --color-text-secondary: #D6D3D1; --color-text-muted: #A8A29E;
|
||||||
--color-border: #3D3836; --color-border-focus: #2DD4BF;
|
--color-border: #3D3836; --color-border-focus: #2DD4BF;
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
--color-primary: #A8A29E; --color-primary-hover: #D6D3D1; --color-primary-light: #292524;
|
--color-primary: #A8A29E; --color-primary-hover: #D6D3D1; --color-primary-light: #292524;
|
||||||
--color-secondary: #A8A29E; --color-secondary-hover: #D6D3D1;
|
--color-secondary: #A8A29E; --color-secondary-hover: #D6D3D1;
|
||||||
--color-accent: #2DD4BF; --color-accent-hover: #5EEAD4; --color-accent-text: #042F2E;
|
--color-accent: #2DD4BF; --color-accent-hover: #5EEAD4; --color-accent-text: #042F2E;
|
||||||
--color-success: #4ADE80; --color-warning: #FBBF24; --color-error: #F87171;
|
--color-success: #4ADE80; --color-success-text: #042F2E; --color-warning: #FBBF24; --color-error: #F87171;
|
||||||
--color-bg: #171717; --color-bg-secondary: #1C1917; --color-bg-tertiary: #292524;
|
--color-bg: #171717; --color-bg-secondary: #1C1917; --color-bg-tertiary: #292524;
|
||||||
--color-text: #F5F5F4; --color-text-secondary: #D6D3D1; --color-text-muted: #A8A29E;
|
--color-text: #F5F5F4; --color-text-secondary: #D6D3D1; --color-text-muted: #A8A29E;
|
||||||
--color-border: #3D3836; --color-border-focus: #2DD4BF;
|
--color-border: #3D3836; --color-border-focus: #2DD4BF;
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ class ChatWidget extends HTMLElement {
|
|||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (this.unsubscribe) this.unsubscribe()
|
if (this.unsubscribe) this.unsubscribe()
|
||||||
if (this.i18nUnsubscribe) this.i18nUnsubscribe()
|
if (this.i18nUnsubscribe) this.i18nUnsubscribe()
|
||||||
|
this._stopPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
_startPolling() {
|
||||||
|
this._stopPolling()
|
||||||
|
this._pollTimer = setInterval(() => this.refreshMessages(), 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopPolling() {
|
||||||
|
if (this._pollTimer) {
|
||||||
|
clearInterval(this._pollTimer)
|
||||||
|
this._pollTimer = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
@@ -66,6 +79,8 @@ class ChatWidget extends HTMLElement {
|
|||||||
|
|
||||||
this.unsubscribe = conversationsService.subscribe(() => this.refreshMessages())
|
this.unsubscribe = conversationsService.subscribe(() => this.refreshMessages())
|
||||||
this.i18nUnsubscribe = i18n.subscribe(() => this.render())
|
this.i18nUnsubscribe = i18n.subscribe(() => this.render())
|
||||||
|
|
||||||
|
if (this.conversation) this._startPolling()
|
||||||
}
|
}
|
||||||
|
|
||||||
async initConversation() {
|
async initConversation() {
|
||||||
@@ -130,9 +145,12 @@ class ChatWidget extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshMessages() {
|
async refreshMessages() {
|
||||||
|
const prevCount = this.messages.length
|
||||||
await this.loadMessages()
|
await this.loadMessages()
|
||||||
this.renderMessages()
|
if (this.messages.length !== prevCount) {
|
||||||
this.scrollToBottom()
|
this.renderMessages()
|
||||||
|
this.scrollToBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDealState() {
|
async loadDealState() {
|
||||||
|
|||||||
@@ -255,8 +255,8 @@ style.textContent = /* css */`
|
|||||||
}
|
}
|
||||||
|
|
||||||
listing-card .payment-published {
|
listing-card .payment-published {
|
||||||
background: var(--color-accent);
|
background: var(--color-success);
|
||||||
color: var(--color-accent-text, #fff);
|
color: var(--color-success-text, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
listing-card .payment-expired {
|
listing-card .payment-expired {
|
||||||
|
|||||||
@@ -1299,11 +1299,13 @@ style.textContent = /* css */`
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
page-listing .btn-copy {
|
page-listing .btn-copy {
|
||||||
padding: var(--space-sm);
|
padding: var(--space-sm);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
page-listing .btn-copy.copied {
|
page-listing .btn-copy.copied {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { directus } from './directus.js'
|
import { directus } from './directus.js'
|
||||||
import { cryptoService } from './crypto.js'
|
import { cryptoService } from './crypto.js'
|
||||||
|
import { auth } from './auth.js'
|
||||||
|
|
||||||
class ConversationsService {
|
class ConversationsService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -140,7 +141,8 @@ class ConversationsService {
|
|||||||
let text = '[Encrypted]'
|
let text = '[Encrypted]'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
text = cryptoService.decrypt(msg.content_encrypted, msg.nonce, otherPublicKey, mySecretKey)
|
const decrypted = cryptoService.decrypt(msg.content_encrypted, msg.nonce, otherPublicKey, mySecretKey)
|
||||||
|
text = decrypted || '[Decryption failed]'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
text = '[Decryption failed]'
|
text = '[Decryption failed]'
|
||||||
}
|
}
|
||||||
@@ -203,12 +205,14 @@ class ConversationsService {
|
|||||||
return this.getConversation(existing.id)
|
return this.getConversation(existing.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await auth.getUser()
|
||||||
const newConv = await directus.startConversation(
|
const newConv = await directus.startConversation(
|
||||||
listingId,
|
listingId,
|
||||||
myHash,
|
myHash,
|
||||||
sellerHash,
|
sellerHash,
|
||||||
myPublicKey,
|
myPublicKey,
|
||||||
sellerPublicKey
|
sellerPublicKey,
|
||||||
|
user?.id
|
||||||
)
|
)
|
||||||
|
|
||||||
return this.getConversation(newConv.id)
|
return this.getConversation(newConv.id)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
*
|
*
|
||||||
* Secret keys are encrypted at rest using AES-GCM with a key
|
* Secret keys are encrypted at rest using AES-GCM with a key
|
||||||
* derived from the user's UUID via PBKDF2.
|
* derived from the user's UUID via PBKDF2.
|
||||||
|
*
|
||||||
|
* Storage keys are namespaced per account (UUID hash prefix)
|
||||||
|
* to support multiple accounts in the same browser.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const STORAGE_KEY = 'kashilo_keypair'
|
const STORAGE_PREFIX = 'kashilo_'
|
||||||
const SALT_KEY = 'kashilo_keypair_salt'
|
|
||||||
const LISTING_KEYS_STORAGE = 'kashilo_listing_keys'
|
|
||||||
|
|
||||||
class CryptoService {
|
class CryptoService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -16,6 +17,7 @@ class CryptoService {
|
|||||||
this.naclUtil = null
|
this.naclUtil = null
|
||||||
this.keyPair = null
|
this.keyPair = null
|
||||||
this.wrappingKey = null
|
this.wrappingKey = null
|
||||||
|
this._storageKeys = null
|
||||||
this.ready = this.init()
|
this.ready = this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +45,33 @@ class CryptoService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _uuidPrefix(uuid) {
|
||||||
|
const data = new TextEncoder().encode(uuid)
|
||||||
|
const hash = await crypto.subtle.digest('SHA-256', data)
|
||||||
|
return Array.from(new Uint8Array(hash)).slice(0, 4)
|
||||||
|
.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getStorageKeys(uuid) {
|
||||||
|
if (this._storageKeys) return this._storageKeys
|
||||||
|
const prefix = await this._uuidPrefix(uuid)
|
||||||
|
this._storageKeys = {
|
||||||
|
keypair: `${STORAGE_PREFIX}kp_${prefix}`,
|
||||||
|
salt: `${STORAGE_PREFIX}salt_${prefix}`,
|
||||||
|
listingKeys: `${STORAGE_PREFIX}lk_${prefix}`
|
||||||
|
}
|
||||||
|
return this._storageKeys
|
||||||
|
}
|
||||||
|
|
||||||
async deriveKey(uuid) {
|
async deriveKey(uuid) {
|
||||||
const enc = new TextEncoder()
|
const enc = new TextEncoder()
|
||||||
let salt = localStorage.getItem(SALT_KEY)
|
const keys = await this._getStorageKeys(uuid)
|
||||||
|
|
||||||
|
let salt = localStorage.getItem(keys.salt)
|
||||||
if (!salt) {
|
if (!salt) {
|
||||||
const saltBytes = crypto.getRandomValues(new Uint8Array(16))
|
const saltBytes = crypto.getRandomValues(new Uint8Array(16))
|
||||||
salt = Array.from(saltBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
salt = Array.from(saltBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
localStorage.setItem(SALT_KEY, salt)
|
localStorage.setItem(keys.salt, salt)
|
||||||
}
|
}
|
||||||
const saltBytes = new Uint8Array(salt.match(/.{2}/g).map(h => parseInt(h, 16)))
|
const saltBytes = new Uint8Array(salt.match(/.{2}/g).map(h => parseInt(h, 16)))
|
||||||
|
|
||||||
@@ -92,10 +114,11 @@ class CryptoService {
|
|||||||
|
|
||||||
async unlock(uuid) {
|
async unlock(uuid) {
|
||||||
await this.ready
|
await this.ready
|
||||||
|
const keys = await this._getStorageKeys(uuid)
|
||||||
const wrappingKey = await this.deriveKey(uuid)
|
const wrappingKey = await this.deriveKey(uuid)
|
||||||
this.wrappingKey = wrappingKey
|
this.wrappingKey = wrappingKey
|
||||||
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(keys.keypair)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored)
|
const parsed = JSON.parse(stored)
|
||||||
@@ -119,10 +142,61 @@ class CryptoService {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load keypair, generating new one')
|
console.warn('Failed to load keypair, generating new one')
|
||||||
|
localStorage.removeItem(keys.listingKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.generateKeyPair(wrappingKey)
|
const migrated = await this._migrateOldStorage(uuid, wrappingKey, keys)
|
||||||
|
if (!migrated) await this.generateKeyPair(wrappingKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _migrateOldStorage(uuid, wrappingKey, keys) {
|
||||||
|
const oldKeypair = localStorage.getItem('kashilo_keypair')
|
||||||
|
const oldSalt = localStorage.getItem('kashilo_keypair_salt')
|
||||||
|
if (!oldKeypair || !oldSalt) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oldSaltBytes = new Uint8Array(oldSalt.match(/.{2}/g).map(h => parseInt(h, 16)))
|
||||||
|
const enc = new TextEncoder()
|
||||||
|
const baseKey = await crypto.subtle.importKey(
|
||||||
|
'raw', enc.encode(uuid), 'PBKDF2', false, ['deriveKey']
|
||||||
|
)
|
||||||
|
const oldWrappingKey = await crypto.subtle.deriveKey(
|
||||||
|
{ name: 'PBKDF2', salt: oldSaltBytes, iterations: 100000, hash: 'SHA-256' },
|
||||||
|
baseKey,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
|
||||||
|
const parsed = JSON.parse(oldKeypair)
|
||||||
|
if (parsed.ct && parsed.iv) {
|
||||||
|
const data = await this.decryptFromStorage(parsed, oldWrappingKey)
|
||||||
|
this.keyPair = {
|
||||||
|
publicKey: this.naclUtil.decodeBase64(data.publicKey),
|
||||||
|
secretKey: this.naclUtil.decodeBase64(data.secretKey)
|
||||||
|
}
|
||||||
|
await this.saveKeyPair(wrappingKey)
|
||||||
|
|
||||||
|
const oldListing = localStorage.getItem('kashilo_listing_keys')
|
||||||
|
if (oldListing) {
|
||||||
|
try {
|
||||||
|
const listingData = await this.decryptFromStorage(JSON.parse(oldListing), oldWrappingKey)
|
||||||
|
const encrypted = await this.encryptForStorage(listingData, wrappingKey)
|
||||||
|
localStorage.setItem(keys.listingKeys, JSON.stringify(encrypted))
|
||||||
|
localStorage.removeItem('kashilo_listing_keys')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('kashilo_keypair')
|
||||||
|
localStorage.removeItem('kashilo_keypair_salt')
|
||||||
|
console.debug('[crypto] migrated old storage to namespaced keys')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug('[crypto] old storage migration failed (different account)')
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateKeyPair(wrappingKey) {
|
async generateKeyPair(wrappingKey) {
|
||||||
@@ -136,20 +210,27 @@ class CryptoService {
|
|||||||
secretKey: this.naclUtil.encodeBase64(this.keyPair.secretKey)
|
secretKey: this.naclUtil.encodeBase64(this.keyPair.secretKey)
|
||||||
}
|
}
|
||||||
const encrypted = await this.encryptForStorage(data, wrappingKey)
|
const encrypted = await this.encryptForStorage(data, wrappingKey)
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(encrypted))
|
localStorage.setItem(this._storageKeys.keypair, JSON.stringify(encrypted))
|
||||||
}
|
}
|
||||||
|
|
||||||
lock() {
|
lock() {
|
||||||
this.keyPair = null
|
this.keyPair = null
|
||||||
this.wrappingKey = null
|
this.wrappingKey = null
|
||||||
|
this._storageKeys = null
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyKeyPair() {
|
destroyKeyPair() {
|
||||||
this.keyPair = null
|
this.keyPair = null
|
||||||
this.wrappingKey = null
|
this.wrappingKey = null
|
||||||
localStorage.removeItem(STORAGE_KEY)
|
if (this._storageKeys) {
|
||||||
localStorage.removeItem(SALT_KEY)
|
localStorage.removeItem(this._storageKeys.keypair)
|
||||||
localStorage.removeItem(LISTING_KEYS_STORAGE)
|
localStorage.removeItem(this._storageKeys.salt)
|
||||||
|
localStorage.removeItem(this._storageKeys.listingKeys)
|
||||||
|
}
|
||||||
|
localStorage.removeItem('kashilo_keypair')
|
||||||
|
localStorage.removeItem('kashilo_keypair_salt')
|
||||||
|
localStorage.removeItem('kashilo_listing_keys')
|
||||||
|
this._storageKeys = null
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublicKey() {
|
getPublicKey() {
|
||||||
@@ -158,8 +239,8 @@ class CryptoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getListingKeysStore() {
|
async getListingKeysStore() {
|
||||||
if (!this.wrappingKey) return {}
|
if (!this.wrappingKey || !this._storageKeys) return {}
|
||||||
const stored = localStorage.getItem(LISTING_KEYS_STORAGE)
|
const stored = localStorage.getItem(this._storageKeys.listingKeys)
|
||||||
if (!stored) return {}
|
if (!stored) return {}
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored)
|
const parsed = JSON.parse(stored)
|
||||||
@@ -173,7 +254,7 @@ class CryptoService {
|
|||||||
async saveListingKeysStore(store) {
|
async saveListingKeysStore(store) {
|
||||||
if (!this.wrappingKey) throw new Error('Not unlocked')
|
if (!this.wrappingKey) throw new Error('Not unlocked')
|
||||||
const encrypted = await this.encryptForStorage(store, this.wrappingKey)
|
const encrypted = await this.encryptForStorage(store, this.wrappingKey)
|
||||||
localStorage.setItem(LISTING_KEYS_STORAGE, JSON.stringify(encrypted))
|
localStorage.setItem(this._storageKeys.listingKeys, JSON.stringify(encrypted))
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateListingKeyPair(listingId) {
|
async generateListingKeyPair(listingId) {
|
||||||
|
|||||||
@@ -54,16 +54,17 @@ export async function sendMessage(conversationId, senderHash, encryptedContent,
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directus sets user_created automatically for authenticated requests
|
export async function startConversation(listingId, participantHash1, participantHash2, publicKey1, publicKey2, buyerUserId) {
|
||||||
export async function startConversation(listingId, participantHash1, participantHash2, publicKey1, publicKey2) {
|
const payload = {
|
||||||
const response = await client.post('/items/conversations', {
|
|
||||||
listing_id: listingId,
|
listing_id: listingId,
|
||||||
participant_hash_1: participantHash1,
|
participant_hash_1: participantHash1,
|
||||||
participant_hash_2: participantHash2,
|
participant_hash_2: participantHash2,
|
||||||
public_key_1: publicKey1,
|
public_key_1: publicKey1,
|
||||||
public_key_2: publicKey2,
|
public_key_2: publicKey2,
|
||||||
status: 'active'
|
status: 'active'
|
||||||
})
|
}
|
||||||
|
if (buyerUserId) payload.buyer_user = buyerUserId
|
||||||
|
const response = await client.post('/items/conversations', payload)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { directus } from './directus.js'
|
import { directus } from './directus.js'
|
||||||
|
import { client } from './directus/client.js'
|
||||||
import { auth } from './auth.js'
|
import { auth } from './auth.js'
|
||||||
|
|
||||||
class NotificationsService {
|
class NotificationsService {
|
||||||
@@ -68,8 +69,11 @@ class NotificationsService {
|
|||||||
document.removeEventListener('visibilitychange', this._onVisibility)
|
document.removeEventListener('visibilitychange', this._onVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVisibility = () => {
|
_onVisibility = async () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
|
if (client._refreshPromise) {
|
||||||
|
await client._refreshPromise
|
||||||
|
}
|
||||||
this.refresh()
|
this.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,13 +69,17 @@ export async function verifySolution(solution) {
|
|||||||
})
|
})
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
if (!response.ok) {
|
||||||
|
const errData = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(errData.error || `HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return { ok: data.ok === true, token: data.captcha_token || null }
|
return { ok: data.ok === true, token: data.captcha_token || null }
|
||||||
} catch {
|
} catch (e) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
return { ok: false, token: null }
|
console.warn('[pow] verify failed:', e.message)
|
||||||
|
return { ok: false, token: null, error: e.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user