mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
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:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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"')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 3–20 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 8–72 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." },
|
||||
|
||||
@@ -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 3–20 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 8–72 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." },
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user