ENH - rebuild VaultX profile management

Move profile identity edits into a secure sheet, add header sign out, persist preferences immediately, and support authenticated password rotation with the existing scrypt provider.
This commit is contained in:
smx.pusha
2026-08-18 10:49:36 +02:00
parent 3574ceaf8b
commit 69d55cc9da
8 changed files with 265 additions and 93 deletions
+4 -2
View File
@@ -130,7 +130,8 @@ describe('crypto store', () => {
await crypto.updateProfile({
handle: 'skyline',
hideBalances: true,
password: '',
currentPassword: '',
newPassword: '',
priceAlerts: false,
tradeConfirmations: true,
}),
@@ -138,7 +139,8 @@ describe('crypto store', () => {
expect(mockNuiCall).toHaveBeenCalledWith('crypto:update-profile', {
handle: 'skyline',
hideBalances: true,
password: '',
currentPassword: '',
newPassword: '',
priceAlerts: false,
tradeConfirmations: true,
})
+2 -1
View File
@@ -113,7 +113,8 @@ export const useCryptoStore = defineStore('crypto', {
async updateProfile(payload: {
handle: string
hideBalances: boolean
password: string
currentPassword: string
newPassword: string
priceAlerts: boolean
tradeConfirmations: boolean
}): Promise<boolean> {
+12 -2
View File
@@ -535,10 +535,20 @@ const cryptoFallbackLocales = {
hideBalances: 'Privacy mode',
hideBalancesBody: 'Hide balances across VaultX.',
identity: 'Profile identity',
passwordForChange: 'Password for handle change',
passwordOptional: 'Only required when changing handle',
edit: 'Edit',
account: 'Account & security',
editTitle: 'Edit profile',
editBody:
'Update your handle or set a new password. Your current password confirms identity changes.',
currentPassword: 'Current password',
currentPasswordPlaceholder: 'Required for handle or password changes',
newPassword: 'New password',
newPasswordPlaceholder: 'Leave blank to keep your current password',
passwordSecurity:
'Passwords use memory-hard hashing and are never shown again.',
saved: 'Profile saved securely.',
save: 'Save profile',
saveChanges: 'Save changes',
securityTitle: 'Protected profile',
securityBody:
'Your profile stays bound to this framework character and financial session.',
@@ -58,6 +58,18 @@ describe('VaultX crypto app contracts', () => {
expect(server).toContain('`price_alerts`')
})
it('supports secure profile editing and header sign out', () => {
expect(source).toContain('class="profile-signout"')
expect(source).toContain('class="profile-edit-button"')
expect(source).toContain("sheet.value = 'profile'")
expect(source).toContain('v-model="profileCurrentPassword"')
expect(source).toContain('v-model="profileNewPassword"')
expect(server).toContain(
'verify_password(profile.id, data.currentPassword)',
)
expect(server).toContain('CryptoHashPassword(new_password)')
})
it('uses the premium dashboard hierarchy without manual refresh controls', () => {
expect(source).toContain('class="portfolio-shell"')
expect(source).toContain('class="featured-market"')
+186 -73
View File
@@ -44,7 +44,7 @@ import {
} from '@/ui'
type Tab = 'portfolio' | 'markets' | 'activity' | 'profile'
type Sheet = 'trade' | 'deposit' | 'withdraw' | null
type Sheet = 'trade' | 'deposit' | 'withdraw' | 'profile' | null
const crypto = useCryptoStore()
const phone = usePhoneStore()
const tab = ref<Tab>('portfolio')
@@ -61,7 +61,8 @@ const financialPassword = ref('')
const showPassword = ref(false)
const formError = ref('')
const profileHandle = ref('')
const profilePassword = ref('')
const profileCurrentPassword = ref('')
const profileNewPassword = ref('')
const priceAlerts = ref(true)
const confirmations = ref(true)
const hideBalances = ref(false)
@@ -285,9 +286,19 @@ function openSettlement(next: 'deposit' | 'withdraw') {
financialPassword.value = ''
formError.value = ''
}
function openProfileEditor() {
profileHandle.value = profile.value?.handle ?? ''
profileCurrentPassword.value = ''
profileNewPassword.value = ''
formError.value = ''
saved.value = false
sheet.value = 'profile'
}
function closeSheet() {
sheet.value = null
crypto.pendingQuote = null
profileCurrentPassword.value = ''
profileNewPassword.value = ''
}
async function submitAuth() {
@@ -332,16 +343,36 @@ async function saveProfile() {
const success = await crypto.updateProfile({
handle: profileHandle.value.trim(),
hideBalances: hideBalances.value,
password: profilePassword.value,
currentPassword: profileCurrentPassword.value,
newPassword: profileNewPassword.value,
priceAlerts: priceAlerts.value,
tradeConfirmations: confirmations.value,
})
if (!success) formError.value = errorText(crypto.error)
else {
profilePassword.value = ''
closeSheet()
saved.value = true
}
}
async function savePreferences() {
saved.value = false
formError.value = ''
const success = await crypto.updateProfile({
handle: profile.value?.handle ?? '',
hideBalances: hideBalances.value,
currentPassword: '',
newPassword: '',
priceAlerts: priceAlerts.value,
tradeConfirmations: confirmations.value,
})
if (!success) formError.value = errorText(crypto.error)
else saved.value = true
}
async function signOut() {
closeSheet()
await crypto.logout()
tab.value = 'portfolio'
}
watch(
profile,
@@ -394,6 +425,18 @@ onMounted(() => void crypto.load())
{{ t(`tabs.${tab}`) }}
</strong>
</template>
<template v-if="authenticated && tab === 'profile' && !detail" #right>
<SkyLink
component="button"
icon-only
class="profile-signout"
:aria-label="t('logout')"
:title="t('logout')"
@click="signOut"
>
<LogOut :size="15" />
</SkyLink>
</template>
</SkyNavbar>
<SkyScrollArea
@@ -990,8 +1033,20 @@ onMounted(() => void crypto.load())
}}</span>
<span class="profile-status"><i />{{ t('profile.verified') }}</span>
</div>
<h2>@{{ profile?.handle }}</h2>
<p><ShieldCheck :size="15" />{{ t('profile.verified') }}</p>
<div class="profile-card__identity">
<span>
<h2>@{{ profile?.handle }}</h2>
<p><ShieldCheck :size="15" />{{ t('profile.verified') }}</p>
</span>
<SkyButton
class="profile-edit-button"
rounded
small
@click="openProfileEditor"
>
<Settings2 :size="15" />{{ t('profile.edit') }}
</SkyButton>
</div>
<div class="profile-card__id">
<small>VAULTX ID</small>
<b>{{ profile?.id.slice(0, 8).toUpperCase() }}</b>
@@ -1037,52 +1092,32 @@ onMounted(() => void crypto.load())
><b>{{ t('profile.priceAlerts') }}</b
><small>{{ t('profile.priceAlertsBody') }}</small></span
></span
><SkyToggle v-model="priceAlerts" /></label
><SkyToggle
v-model="priceAlerts"
@change="savePreferences" /></label
><label
><span
><Fingerprint :size="18" /><span
><b>{{ t('profile.confirmations') }}</b
><small>{{ t('profile.confirmationsBody') }}</small></span
></span
><SkyToggle v-model="confirmations" /></label
><SkyToggle
v-model="confirmations"
@change="savePreferences" /></label
><label
><span
><EyeOff :size="18" /><span
><b>{{ t('profile.hideBalances') }}</b
><small>{{ t('profile.hideBalancesBody') }}</small></span
></span
><SkyToggle v-model="hideBalances" /></label
><SkyToggle
v-model="hideBalances"
@change="savePreferences" /></label
></SkyCard>
<h2 class="title">{{ t('profile.identity') }}</h2>
<form class="form profile-form" @submit.prevent="saveProfile">
<SkyField
v-model="profileHandle"
:label="t('auth.handle')"
maxlength="20"
outline
><template #leading><UserRound :size="18" /></template></SkyField
><SkyField
v-model="profilePassword"
:label="t('profile.passwordForChange')"
:placeholder="t('profile.passwordOptional')"
type="password"
outline
><template #leading><LockKeyhole :size="18" /></template
></SkyField>
<p v-if="saved" class="success">
<ShieldCheck :size="15" />{{ t('profile.saved') }}
</p>
<p v-if="formError" class="error">{{ formError }}</p>
<SkyButton block type="submit">{{ t('profile.save') }}</SkyButton>
</form>
<SkyCard class="security"
><ShieldCheck :size="22" /><span
><b>{{ t('profile.securityTitle') }}</b
><small>{{ t('profile.securityBody') }}</small></span
></SkyCard
><SkyButton block variant="danger" @click="crypto.logout()"
><LogOut :size="18" />{{ t('logout') }}</SkyButton
>
<p v-if="saved" class="profile-feedback success">
<ShieldCheck :size="15" />{{ t('profile.saved') }}
</p>
<p v-if="formError" class="profile-feedback error">{{ formError }}</p>
</template>
</SkyScrollArea>
@@ -1140,19 +1175,24 @@ onMounted(() => void crypto.load())
:market="selectedMarket"
/>
<ArrowDownLeft v-else-if="sheet === 'deposit'" :size="19" />
<ArrowUpRight v-else :size="19" />
<ArrowUpRight v-else-if="sheet === 'withdraw'" :size="19" />
<UserRound v-else :size="19" />
</span>
<span>
<small>{{
sheet === 'trade' && selectedMarket
? selectedMarket.name
: t('activity.cash')
: sheet === 'profile'
? t('profile.account')
: t('activity.cash')
}}</small>
<h2>
{{
sheet === 'trade'
? `${t(`trade.${side}`)} ${selectedMarket?.symbol}`
: t(`actions.${sheet}`)
: sheet === 'profile'
? t('profile.editTitle')
: t(`actions.${sheet}`)
}}
</h2>
</span>
@@ -1222,7 +1262,43 @@ onMounted(() => void crypto.load())
t(crypto.pendingQuote ? 'trade.confirm' : 'trade.getQuote')
}}</SkyButton
></template
><template v-else
><template v-else-if="sheet === 'profile'">
<p class="sheet-copy">{{ t('profile.editBody') }}</p>
<form class="profile-edit-sheet" @submit.prevent="saveProfile">
<SkyField
v-model="profileHandle"
:label="t('auth.handle')"
maxlength="20"
outline
><template #leading><UserRound :size="18" /></template
></SkyField>
<SkyField
v-model="profileCurrentPassword"
:label="t('profile.currentPassword')"
:placeholder="t('profile.currentPasswordPlaceholder')"
type="password"
outline
><template #leading><LockKeyhole :size="18" /></template
></SkyField>
<SkyField
v-model="profileNewPassword"
:label="t('profile.newPassword')"
:placeholder="t('profile.newPasswordPlaceholder')"
type="password"
outline
><template #leading><Fingerprint :size="18" /></template
></SkyField>
<div class="profile-edit-note">
<ShieldCheck :size="17" />
<span>{{ t('profile.passwordSecurity') }}</span>
</div>
<p v-if="formError" class="error">{{ formError }}</p>
<SkyButton block type="submit">{{
t('profile.saveChanges')
}}</SkyButton>
</form>
</template>
<template v-else
><p class="sheet-copy">
{{
t(
@@ -1743,27 +1819,6 @@ onMounted(() => void crypto.load())
color: var(--muted);
font-size: 11px;
}
.profile-form {
padding: 15px;
background: var(--card);
border-radius: 20px;
}
.security {
display: flex;
gap: 11px;
align-items: center;
margin: 15px 0;
padding: 15px;
color: #49e4b2;
background: rgba(49, 214, 170, 0.08);
}
.security span {
display: grid;
}
.security small {
color: var(--muted);
font-size: 11px;
}
.sheet {
display: grid;
gap: 13px;
@@ -1845,6 +1900,19 @@ onMounted(() => void crypto.load())
font-size: 17px;
letter-spacing: -0.02em;
}
.profile-signout {
width: 34px;
height: 34px;
min-height: 34px;
padding: 0;
color: #fff;
background: rgba(255, 92, 112, 0.1);
border: 1px solid rgba(255, 117, 136, 0.18);
border-radius: 50%;
}
.profile-signout svg {
color: #ff7588;
}
.vault-detail-title {
display: inline-flex;
gap: 8px;
@@ -2641,7 +2709,7 @@ onMounted(() => void crypto.load())
}
.profile-card {
position: relative;
min-height: 225px;
min-height: 244px;
padding: 18px;
overflow: hidden;
background:
@@ -2698,13 +2766,26 @@ onMounted(() => void crypto.load())
color: var(--vault-mint);
font-size: 11px;
}
.profile-card h2 {
.profile-card__identity {
position: relative;
margin: 14px 0 2px;
font-size: 24px;
display: flex;
gap: 12px;
align-items: end;
justify-content: space-between;
margin-top: 15px;
}
.profile-card > p {
position: relative;
.profile-card__identity > span {
min-width: 0;
flex: 1;
}
.profile-card h2 {
margin: 0 0 3px;
font-size: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-card__identity p {
display: flex;
gap: 6px;
align-items: center;
@@ -2712,9 +2793,20 @@ onMounted(() => void crypto.load())
color: var(--muted);
font-size: 11px;
}
.profile-card > p svg {
.profile-card__identity p svg {
color: var(--vault-mint);
}
.profile-edit-button {
width: auto;
min-width: auto;
min-height: 36px;
flex: 0 0 auto;
padding: 0 12px;
color: #fff;
background: rgba(101, 251, 210, 0.1);
border: 1px solid rgba(101, 251, 210, 0.2);
box-shadow: inset 0 1px rgba(255, 255, 255, 0.06);
}
.profile-card__id {
position: relative;
display: grid;
@@ -2748,6 +2840,9 @@ onMounted(() => void crypto.load())
background: linear-gradient(145deg, #151c25, #0b1016);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.profile-feedback {
margin: 10px 3px 0;
}
.security-overview {
display: flex;
gap: 13px;
@@ -2802,8 +2897,6 @@ onMounted(() => void crypto.load())
line-height: 1.35;
}
.settings,
.profile-form,
.security,
.stats,
.investment,
.movers {
@@ -3003,6 +3096,26 @@ onMounted(() => void crypto.load())
font-size: 12px;
line-height: 1.5;
}
.profile-edit-sheet {
display: grid;
gap: 12px;
}
.profile-edit-note {
display: flex;
gap: 9px;
align-items: flex-start;
padding: 11px 12px;
color: rgba(255, 255, 255, 0.62);
font-size: 11px;
line-height: 1.4;
background: rgba(101, 251, 210, 0.06);
border: 1px solid rgba(101, 251, 210, 0.12);
border-radius: 14px;
}
.profile-edit-note svg {
flex: 0 0 auto;
color: var(--vault-mint);
}
.quote {
background: linear-gradient(145deg, #151d27, #0c1117);
border: 1px solid rgba(255, 255, 255, 0.07);
+1 -1
View File
@@ -1063,7 +1063,7 @@ Locales["de"] = {
trade = { buy = "Kaufen", sell = "Verkaufen", quantity = "Menge", available = "Verfügbar:", price = "Angebotspreis", gross = "Brutto", fee = "Börsengebühr", total = "Gesamt", quoteExpiry = "Dieses Serverangebot läuft nach 8 Sekunden ab und kann nur einmal genutzt werden.", getQuote = "Geschütztes Angebot", confirm = "Handel bestätigen" },
settlement = { depositBody = "Übertrage Bankguthaben in dein verbuchtes VaultX-Guthaben.", withdrawBody = "Zahle verbuchtes VaultX-Guthaben auf das Bankkonto deines Charakters aus.", amount = "Ganzer Bankbetrag", confirm = "Sicher bestätigen" },
activity = { title = "Finanzaktivität", empty = "Keine Aktivität", emptyBody = "Einzahlungen, Auszahlungen und Handel erscheinen hier.", volume = "Handelsvolumen gesamt", completedTrades = "abgeschlossene Trades", cash = "Guthaben", filters = { all = "Alle", trades = "Trades", wallet = "Wallet" } },
profile = { verified = "Charakterbesitz verifiziert", trades = "Trades", volume = "Volumen", memberSince = "Mitglied seit", preferences = "Einstellungen", priceAlerts = "Kurswarnungen", priceAlertsBody = "Bei auffälligen Marktbewegungen benachrichtigen.", confirmations = "Handelsbestätigung", confirmationsBody = "Zusätzliche Bestätigung vor der Ausführung behalten.", hideBalances = "Privatsphäre-Modus", hideBalancesBody = "Beträge in VaultX verbergen.", identity = "Profilidentität", passwordForChange = "Passwort für Benutzernamenänderung", passwordOptional = "Nur bei Änderung des Benutzernamens nötig", saved = "Profil sicher gespeichert.", save = "Profil speichern", securityTitle = "Geschütztes Profil", securityBody = "Dein Profil bleibt an diesen Framework-Charakter und die Finanzsitzung gebunden." },
profile = { verified = "Charakterbesitz verifiziert", trades = "Trades", volume = "Volumen", memberSince = "Mitglied seit", preferences = "Einstellungen", priceAlerts = "Kurswarnungen", priceAlertsBody = "Bei auffälligen Marktbewegungen benachrichtigen.", confirmations = "Handelsbestätigung", confirmationsBody = "Zusätzliche Bestätigung vor der Ausführung behalten.", hideBalances = "Privatsphäre-Modus", hideBalancesBody = "Beträge in VaultX verbergen.", identity = "Profilidentität", edit = "Bearbeiten", account = "Konto & Sicherheit", editTitle = "Profil bearbeiten", editBody = "Ändere deinen Benutzernamen oder lege ein neues Passwort fest. Dein aktuelles Passwort bestätigt Identitätsänderungen.", currentPassword = "Aktuelles Passwort", currentPasswordPlaceholder = "Für Namens- oder Passwortänderungen erforderlich", newPassword = "Neues Passwort", newPasswordPlaceholder = "Leer lassen, um das aktuelle Passwort zu behalten", passwordSecurity = "Passwörter werden speicherintensiv gehasht und nie wieder angezeigt.", saved = "Profil sicher gespeichert.", save = "Profil speichern", saveChanges = "Änderungen speichern", securityTitle = "Geschütztes Profil", securityBody = "Dein Profil bleibt an diesen Framework-Charakter und die Finanzsitzung gebunden." },
activityTypes = { buy = "Asset-Kauf", sell = "Asset-Verkauf", deposit = "Bankeinzahlung", withdrawal = "Bankauszahlung" },
statuses = { completed = "Abgeschlossen", pending = "Prüfung ausstehend", failed = "Fehlgeschlagen", manual_review = "Manuelle Prüfung" },
errors = { invalid_profile = "Prüfe deine VaultX-Profileinstellungen.", invalid_handle = "Nutze 320 Buchstaben, Zahlen, Punkte oder Unterstriche.", handle_taken = "Dieser VaultX-Benutzername ist bereits vergeben.", profile_exists = "Dieser Charakter besitzt bereits ein VaultX-Profil.", invalid_password = "Das Passwort muss 872 Zeichen lang sein.", invalid_credentials = "Das Passwort ist falsch.", locked = "Zu viele Versuche. Versuche es später erneut.", not_authenticated = "Logge dich zuerst bei VaultX ein.", invalid_amount = "Gib einen positiven ganzen Betrag ein.", invalid_quantity = "Gib eine Menge mit bis zu sechs Nachkommastellen ein.", insufficient_funds = "Dein verfügbares Guthaben reicht nicht aus.", insufficient_liquidity = "Die Börse kann diese Menge derzeit nicht ausführen.", quote_expired = "Das Angebot ist abgelaufen. Fordere ein neues an.", quote_unavailable = "Das Angebot ist nicht mehr verfügbar.", market_unavailable = "Dieser Markt ist vorübergehend pausiert.", limit_exceeded = "Die Anfrage überschreitet ein Börsenlimit.", duplicate_request = "Diese Anfrage wurde bereits verarbeitet.", rate_limited = "Zu viele Anfragen. Versuche es gleich erneut.", settlement_pending = "Eine Geldbewegung wartet bereits auf Prüfung.", service_unavailable = "VaultX ist vorübergehend nicht verfügbar.", request_failed = "Die sichere Börsenanfrage ist fehlgeschlagen.", default = "VaultX konnte die Anfrage nicht abschließen." },
+1 -1
View File
@@ -1063,7 +1063,7 @@ Locales["en"] = {
trade = { buy = "Buy", sell = "Sell", quantity = "Quantity", available = "Available:", price = "Quote price", gross = "Gross", fee = "Exchange fee", total = "Total", quoteExpiry = "This server quote expires after 8 seconds and can only be used once.", getQuote = "Get protected quote", confirm = "Confirm trade" },
settlement = { depositBody = "Move bank funds into your settled VaultX cash balance.", withdrawBody = "Return settled VaultX cash to your character's bank account.", amount = "Whole bank amount", confirm = "Confirm securely" },
activity = { title = "Financial activity", empty = "No activity", emptyBody = "Deposits, withdrawals and trades will appear here.", volume = "Lifetime trading volume", completedTrades = "completed trades", cash = "Cash", filters = { all = "All", trades = "Trades", wallet = "Wallet" } },
profile = { verified = "Character ownership verified", trades = "Trades", volume = "Volume", memberSince = "Member since", preferences = "Preferences", priceAlerts = "Price alerts", priceAlertsBody = "Notify me about notable market moves.", confirmations = "Trade confirmations", confirmationsBody = "Keep an extra confirmation before execution.", hideBalances = "Privacy mode", hideBalancesBody = "Hide balances across VaultX.", identity = "Profile identity", passwordForChange = "Password for handle change", passwordOptional = "Only required when changing handle", saved = "Profile saved securely.", save = "Save profile", securityTitle = "Protected profile", securityBody = "Your profile stays bound to this framework character and financial session." },
profile = { verified = "Character ownership verified", trades = "Trades", volume = "Volume", memberSince = "Member since", preferences = "Preferences", priceAlerts = "Price alerts", priceAlertsBody = "Notify me about notable market moves.", confirmations = "Trade confirmations", confirmationsBody = "Keep an extra confirmation before execution.", hideBalances = "Privacy mode", hideBalancesBody = "Hide balances across VaultX.", identity = "Profile identity", edit = "Edit", account = "Account & security", editTitle = "Edit profile", editBody = "Update your handle or set a new password. Your current password confirms identity changes.", currentPassword = "Current password", currentPasswordPlaceholder = "Required for handle or password changes", newPassword = "New password", newPasswordPlaceholder = "Leave blank to keep your current password", passwordSecurity = "Passwords use memory-hard hashing and are never shown again.", saved = "Profile saved securely.", save = "Save profile", saveChanges = "Save changes", securityTitle = "Protected profile", securityBody = "Your profile stays bound to this framework character and financial session." },
activityTypes = { buy = "Asset purchase", sell = "Asset sale", deposit = "Bank deposit", withdrawal = "Bank withdrawal" },
statuses = { completed = "Completed", pending = "Pending review", failed = "Failed", manual_review = "Manual review" },
errors = { invalid_profile = "Check your VaultX profile settings.", invalid_handle = "Use 320 letters, numbers, dots or underscores.", handle_taken = "That VaultX handle is already taken.", profile_exists = "This character already owns a VaultX profile.", invalid_password = "Password must be 872 characters.", invalid_credentials = "The password is incorrect.", locked = "Too many attempts. Try again later.", not_authenticated = "Sign in to VaultX first.", invalid_amount = "Enter a positive whole amount.", invalid_quantity = "Enter a quantity with up to six decimals.", insufficient_funds = "Your available balance is too low.", insufficient_liquidity = "The exchange cannot fill that amount right now.", quote_expired = "This quote expired. Request a new one.", quote_unavailable = "The quote is no longer available.", market_unavailable = "This market is temporarily paused.", limit_exceeded = "This request exceeds an exchange limit.", duplicate_request = "This request was already processed.", rate_limited = "Too many requests. Try again shortly.", settlement_pending = "A money transfer is already pending review.", service_unavailable = "VaultX is temporarily unavailable.", request_failed = "The secure exchange request failed.", default = "VaultX could not complete the request." },
+47 -13
View File
@@ -671,11 +671,20 @@ Bridge.Callbacks.Register("sky_phone:crypto:update-profile", function(source, da
then
return { success = false, error = "invalid_profile" }
end
if handle:lower() ~= profile.handle:lower() then
if not verify_password(profile.id, data.password) then
local handle_changed = handle:lower() ~= profile.handle:lower()
local new_password = type(data.newPassword) == "string" and data.newPassword or ""
local password_changed = new_password ~= ""
if password_changed and not valid_password(new_password) then
return { success = false, error = "invalid_password" }
end
if handle_changed or password_changed then
if not verify_password(profile.id, data.currentPassword) then
audit(profile.id, profile.owner_identifier, "profile_update_reauth_failed", "")
return { success = false, error = "invalid_credentials" }
end
sessions[source].recently_authenticated_at = os.time()
end
if handle_changed then
local duplicate = Bridge.Database.Query(
"SELECT 1 FROM `sky_phone_crypto_profiles` WHERE `handle` = ? AND `id` <> ? LIMIT 1",
{ handle, profile.id }
@@ -684,18 +693,43 @@ Bridge.Callbacks.Register("sky_phone:crypto:update-profile", function(source, da
return { success = false, error = "handle_taken" }
end
end
Bridge.Database.Query([[
UPDATE `sky_phone_crypto_profiles`
SET `handle` = ?, `price_alerts` = ?, `trade_confirmations` = ?, `hide_balances` = ?
WHERE `id` = ?
]], {
handle,
data.priceAlerts and 1 or 0,
data.tradeConfirmations and 1 or 0,
data.hideBalances and 1 or 0,
if password_changed then
local password_hash = exports[GetCurrentResourceName()]:CryptoHashPassword(new_password)
if type(password_hash) ~= "string" then
return { success = false, error = "service_unavailable" }
end
Bridge.Database.Query([[
UPDATE `sky_phone_crypto_profiles`
SET `handle` = ?, `password_hash` = ?, `price_alerts` = ?,
`trade_confirmations` = ?, `hide_balances` = ?
WHERE `id` = ?
]], {
handle,
password_hash,
data.priceAlerts and 1 or 0,
data.tradeConfirmations and 1 or 0,
data.hideBalances and 1 or 0,
profile.id,
})
else
Bridge.Database.Query([[
UPDATE `sky_phone_crypto_profiles`
SET `handle` = ?, `price_alerts` = ?, `trade_confirmations` = ?, `hide_balances` = ?
WHERE `id` = ?
]], {
handle,
data.priceAlerts and 1 or 0,
data.tradeConfirmations and 1 or 0,
data.hideBalances and 1 or 0,
profile.id,
})
end
audit(
profile.id,
})
audit(profile.id, profile.owner_identifier, "profile_updated", handle)
profile.owner_identifier,
"profile_updated",
(handle_changed and "handle" or "preferences") .. (password_changed and ",password" or "")
)
return { success = true, data = bootstrap(profile_by_owner(profile.owner_identifier)) }
end)