This commit is contained in:
2026-05-23 17:17:56 -07:00
commit 448f2e33ef
135 changed files with 11817 additions and 0 deletions
@@ -0,0 +1,136 @@
import { setTitle } from '../../functions/Route'
import HeaderMessage from '../layout/HeaderMessage'
import InputButton from '../inputs/Button'
import InputButtonRow from '../inputs/ButtonRow'
import InputLabel from '../inputs/Label'
import InputDescription from '../inputs/Description'
import './styles/Settings.css'
export default function ViewSettings() {
const GIFUU_PREFIX = '_|GIFUU_BACKUP:DO_NOT_SHARE|_'
async function onDataBackup() {
const password = prompt('Please enter a password for this backup (Leave empty for none):')
if (password === null) return
// Generate Key
const enc = new TextEncoder()
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
const salt = crypto.getRandomValues(new Uint8Array(16))
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
)
// Encrypt String
const data = JSON.stringify(localStorage)
const nonce = crypto.getRandomValues(new Uint8Array(12))
const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, enc.encode(data))
// Package Contents
const bundle = new Uint8Array(salt.byteLength + nonce.byteLength + cipher.byteLength)
bundle.set(salt, 0)
bundle.set(nonce, 16)
bundle.set(new Uint8Array(cipher), 28)
const string = GIFUU_PREFIX + btoa(String.fromCharCode(...bundle))
navigator.clipboard.writeText(string)
alert('Data has been copied to clipboard.')
}
async function onDataRestore() {
// Retrieve User Data
const blob = await navigator.clipboard.readText()
if (!blob.startsWith(GIFUU_PREFIX)) {
alert('Contents stored in clipboard is not a backup.')
return
}
const password = prompt('Please enter the password for this backup:')
if (password === null) return
// Unpackage Contents
const bundle = Uint8Array.from(atob(blob.slice(GIFUU_PREFIX.length)), (c) => c.charCodeAt(0))
const salt = bundle.slice(0, 16)
const iv = bundle.slice(16, 28)
const cipher = bundle.slice(28)
// Generate Key
const enc = new TextEncoder()
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt'],
)
// Decrypt String
let raw: string
try {
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher)
raw = new TextDecoder().decode(plain)
} catch (err) {
console.error('Backup Error:', err)
alert('Incorrect Password')
return
}
// Parse String
try {
const data = JSON.parse(raw)
if (typeof data !== 'object' || Array.isArray(data)) {
throw 'Invalid Object'
}
Object.entries(data).forEach(([k, v]) => {
if (typeof k === 'string' && typeof v === 'string') {
localStorage.setItem(k, v)
}
})
} catch (err) {
console.error('Backup Error:', err)
alert('Invalid data found in backup.')
}
alert('Data restored, the webpage will now refresh.')
window.location.reload()
}
setTitle('Settings')
return (
<>
<HeaderMessage label="View: Settings" />
<div className="view-settings">
<div className="section">
<InputLabel for="" label="Data Management" />
<InputDescription>
Export your local preferences and tokens for transfer or backup purposes to your devices
clipboard. Do not share these with anybody!
</InputDescription>
<InputButtonRow split={true}>
<InputButton
id="action-backup"
label="Backup"
onClick={onDataBackup}
disabled={false}
rainbow={false}
selected={false}
/>
<InputButton
id="action-restore"
label="Restore"
onClick={onDataRestore}
disabled={false}
rainbow={false}
selected={false}
/>
</InputButtonRow>
</div>
</div>
</>
)
}