200 lines
5.5 KiB
JavaScript
200 lines
5.5 KiB
JavaScript
/**
|
|
* @param {(e: *) => void} errorHandler
|
|
* @param {Backend} backend
|
|
* @param {PasswordCrypto} crypto
|
|
* @param {string} urlSplit
|
|
*/
|
|
function initShare(errorHandler, backend, crypto, urlSplit) {
|
|
const form = document.querySelector("form#share-form")
|
|
const fieldset = document.querySelector("fieldset#share-fieldset")
|
|
const submit = document.querySelector("button#share-submit")
|
|
const dialog = document.querySelector("dialog#share-dialog")
|
|
const link = document.querySelector("input#share-link")
|
|
const copy = document.querySelector("button#share-copy")
|
|
const close = document.querySelector("button#share-close")
|
|
const generate = document.querySelector("a#share-generate")
|
|
const password = document.querySelector("textarea#share-password")
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
const data = new FormData(e.target)
|
|
e.preventDefault()
|
|
|
|
try {
|
|
fieldset.disabled = true
|
|
submit.ariaBusy = "true"
|
|
|
|
const encrypted = await crypto.encryptPassword(data.get("password"))
|
|
const id = await backend.createPassword(
|
|
encrypted.password,
|
|
parseInt(data.get("expires-in")),
|
|
)
|
|
|
|
const url = new URL(window.location)
|
|
url.hash = [id, encrypted.key, encrypted.iv].join(urlSplit)
|
|
url.search = ""
|
|
|
|
link.value = url.toString()
|
|
dialog.showModal()
|
|
} catch (e) {
|
|
errorHandler(e)
|
|
} finally {
|
|
fieldset.disabled = false
|
|
submit.ariaBusy = "false"
|
|
}
|
|
})
|
|
|
|
copy.addEventListener("click", () => {
|
|
link.select()
|
|
link.setSelectionRange(0, 99999)
|
|
navigator.clipboard.writeText(link.value)
|
|
})
|
|
|
|
close.addEventListener("click", () => {
|
|
dialog.close()
|
|
})
|
|
|
|
generate.addEventListener("click", () => {
|
|
let result = ""
|
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
for (let i = 0; i < 12; i++) {
|
|
result += charset.charAt(Math.floor(Math.random() * charset.length))
|
|
}
|
|
|
|
password.value = result
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {(e: *) => void} errorHandler
|
|
* @param {Backend} backend
|
|
* @param {PasswordCrypto} crypto
|
|
* @param {string} urlSplit
|
|
* @param {string} hidden
|
|
*/
|
|
function initView(errorHandler, backend, crypto, urlSplit, hidden) {
|
|
const share = document.querySelector("article#share")
|
|
const loading = document.querySelector("article#loading")
|
|
const confirm = document.querySelector("article#confirm")
|
|
const confirmNo = document.querySelector("button#confirm-no")
|
|
const confirmYes = document.querySelector("button#confirm-yes")
|
|
const notFound = document.querySelector("article#not-found")
|
|
const notFoundOk = document.querySelector("button#not-found-ok")
|
|
const view = document.querySelector("article#view")
|
|
const viewPassword = document.querySelector("textarea#view-password")
|
|
const viewOk = document.querySelector("button#view-ok")
|
|
|
|
const hash = window.location.hash
|
|
if (hash == "" || hash == "#") {
|
|
share.classList.remove(hidden)
|
|
return
|
|
}
|
|
|
|
const raw = hash.substring(1)
|
|
const [id, key, iv] = raw.split(urlSplit)
|
|
|
|
async function load() {
|
|
try {
|
|
loading.classList.remove(hidden)
|
|
|
|
const has = await backend.hasPassword(id)
|
|
if (!has) {
|
|
loading.classList.add(hidden)
|
|
notFound.classList.remove(hidden)
|
|
return
|
|
}
|
|
|
|
loading.classList.add(hidden)
|
|
confirm.classList.remove(hidden)
|
|
} catch (e) {
|
|
errorHandler(e)
|
|
} finally {
|
|
loading.classList.add(hidden)
|
|
}
|
|
}
|
|
|
|
notFoundOk.addEventListener("click", () => {
|
|
notFound.classList.add(hidden)
|
|
share.classList.remove(hidden)
|
|
window.location.hash = ""
|
|
})
|
|
|
|
confirmNo.addEventListener("click", () => {
|
|
confirm.classList.add(hidden)
|
|
share.classList.remove(hidden)
|
|
window.location.hash = ""
|
|
})
|
|
|
|
confirmYes.addEventListener("click", async () => {
|
|
try {
|
|
confirmYes.ariaBusy = "true"
|
|
confirmNo.disabled = true
|
|
confirmYes.disabled = true
|
|
|
|
const encrypted = await backend.getPassword(id)
|
|
const password = await crypto.decryptPassword(encrypted, key, iv)
|
|
|
|
viewPassword.innerText = password
|
|
confirm.classList.add(hidden)
|
|
view.classList.remove(hidden)
|
|
window.location.hash = ""
|
|
} catch (e) {
|
|
errorHandler(e)
|
|
} finally {
|
|
confirmYes.ariaBusy = "false"
|
|
confirmNo.disabled = false
|
|
confirmYes.disabled = false
|
|
}
|
|
})
|
|
|
|
viewOk.addEventListener("click", () => {
|
|
view.classList.add(hidden)
|
|
share.classList.remove(hidden)
|
|
window.location.hash = ""
|
|
})
|
|
|
|
load()
|
|
}
|
|
|
|
/**
|
|
* @param {Language} language
|
|
*/
|
|
function initLanguage(language) {
|
|
const selectLanguages = document.querySelectorAll("li.select-lang")
|
|
for (const selectLang of selectLanguages) {
|
|
const lang = selectLang.getAttribute("data-lang")
|
|
|
|
selectLang.addEventListener("click", async () => {
|
|
language.setLanguage(lang)
|
|
})
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
const dialog = document.querySelector("dialog#error")
|
|
const error = document.querySelector("p#error-error")
|
|
const close = document.querySelector("button#error-close")
|
|
|
|
/**
|
|
* @param {*} error Error
|
|
*/
|
|
function handleError(err) {
|
|
console.error(err)
|
|
error.innerText = err
|
|
dialog.showModal()
|
|
}
|
|
|
|
close.addEventListener("click", () => {
|
|
dialog.close()
|
|
})
|
|
|
|
const backend = new Backend()
|
|
const crypto = new PasswordCrypto()
|
|
const language = new Language(handleError, "en")
|
|
const urlSplit = ":"
|
|
|
|
initShare(handleError, backend, crypto, urlSplit)
|
|
initView(handleError, backend, crypto, urlSplit, "hidden")
|
|
initLanguage(language)
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => init())
|