rework entire front end

This commit is contained in:
1e99 2025-05-23 22:22:18 +02:00
parent fc04b6c9cd
commit a5792c8ad0
18 changed files with 625 additions and 642 deletions

View file

@ -100,6 +100,7 @@ func newStorage() (storage password.Storage, err error) {
return return
} }
// Clear storage at regular intervals
go func() { go func() {
ticker := time.Tick(time.Duration(clearInterval) * time.Second) ticker := time.Tick(time.Duration(clearInterval) * time.Second)
for { for {

View file

@ -1,41 +0,0 @@
package route
import (
"encoding/json"
"net/http"
"git.1e99.eu/1e99/passed/password"
)
func GetPassword(storage password.Storage) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
id := req.PathValue("id")
passwd, err := storage.Get(id)
switch {
case err == password.ErrNotFound:
http.Error(res, "Password not found", http.StatusNotFound)
return
case err != nil:
http.Error(res, "", http.StatusInternalServerError)
return
}
err = storage.Delete(id)
if err != nil {
http.Error(res, "", http.StatusInternalServerError)
return
}
resBody := struct {
// Go automatically encodes byte arrays using Base64
Password []byte `json:"password"`
}{
Password: passwd,
}
err = json.NewEncoder(res).Encode(&resBody)
if err != nil {
http.Error(res, "", http.StatusInternalServerError)
return
}
}
}

View file

@ -1,24 +0,0 @@
package route
import (
"net/http"
"git.1e99.eu/1e99/passed/password"
)
func HasPassword(storage password.Storage) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
id := req.PathValue("id")
_, err := storage.Get(id)
switch {
case err == password.ErrNotFound:
http.Error(res, "", http.StatusNotFound)
return
case err != nil:
http.Error(res, "", http.StatusInternalServerError)
return
}
res.WriteHeader(http.StatusNoContent)
}
}

View file

@ -50,15 +50,54 @@ func CreatePassword(storage password.Storage, maxLength int) http.HandlerFunc {
return return
} }
resBody := struct { err = json.NewEncoder(res).Encode(&id)
Id string `json:"id"`
}{
Id: id,
}
err = json.NewEncoder(res).Encode(&resBody)
if err != nil { if err != nil {
http.Error(res, "", http.StatusInternalServerError) http.Error(res, "", http.StatusInternalServerError)
return return
} }
} }
} }
func GetPassword(storage password.Storage) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
id := req.PathValue("id")
passwd, err := storage.Get(id)
switch {
case err == password.ErrNotFound:
http.Error(res, "Password not found", http.StatusNotFound)
return
case err != nil:
http.Error(res, "", http.StatusInternalServerError)
return
}
err = storage.Delete(id)
if err != nil {
http.Error(res, "", http.StatusInternalServerError)
return
}
err = json.NewEncoder(res).Encode(&passwd)
if err != nil {
http.Error(res, "", http.StatusInternalServerError)
return
}
}
}
func HasPassword(storage password.Storage) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
id := req.PathValue("id")
_, err := storage.Get(id)
switch {
case err == password.ErrNotFound:
http.Error(res, "", http.StatusNotFound)
return
case err != nil:
http.Error(res, "", http.StatusInternalServerError)
return
}
res.WriteHeader(http.StatusNoContent)
}
}

7
static/css/main.css Normal file
View file

@ -0,0 +1,7 @@
.hidden {
display: none;
}
textarea {
font-family: monospace;
}

File diff suppressed because one or more lines are too long

View file

@ -1,129 +1,131 @@
<!doctype html> <!DOCTYPE html>
<html lang="en">
<html> <head>
<head> <meta charset="UTF-8">
<meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!--<link rel="icon" href="favicon.png" />-->
<title>PassED</title>
<link rel="stylesheet" href="/css/pico.min.css" /> <link rel="stylesheet" href="/css/pico.min.css">
<link rel="stylesheet" href="/css/main.css">
<script src="/js/lang.js"></script> <script src="/js/lang.js"></script>
<script src="/js/api.js"></script> <script src="/js/backend.js"></script>
<script src="/js/crypto.js"></script> <script src="/js/crypto.js"></script>
<script src="/js/main.js"></script> <script src="/js/main.js"></script>
</head>
<nav class="container">
<ul>
<li>
<strong t="title"></strong>
</li>
</ul>
<ul> <title>PassED</title>
<li> </head>
<a href="https://git.1e99.eu/1e99/passed/" t="source-code"></a>
</li>
<li> <body>
<details class="dropdown"> <nav class="container">
<summary t="language"></summary> <ul>
<ul dir="rtl"> <li>
<li class="select-language" data-lang="de">Deutsch</li> <strong>PassED</strong>
<li class="select-language" data-lang="en">English</li> </li>
<li class="select-language" data-lang="es">Español</li> </ul>
<li class="select-language" data-lang="fr">Français</li>
<li class="select-language" data-lang="nl">Nederlands</li>
<li class="select-language" data-lang="ro">Română</li>
</ul>
</details>
</li>
</ul>
</nav>
<main class="container"> <ul>
<article> <li>
<header t="enter-password"></header> <a href="https://git.1e99.eu/1e99/passed" t="source"></a>
</li>
<form id="enter-password"> <li>
<fieldset id="enter-password"> <details class="dropdown">
<label> <summary t="language"></summary>
<span t="password"></span> <ul dir="rtl">
<textarea name="password"></textarea> <li class="select-lang" data-lang="de">Deutsch</li>
</label> <li class="select-lang" data-lang="en">English</li>
</ul>
</details>
</li>
</ul>
</nav>
<label> <main class="container">
<span t="expires-in"></span> <article id="loading" class="hidden" aria-busy="true">
<select name="expires-in"> </article>
<option value="3600" selected t="expires-in.1-hour"></option>
<option value="43200" t="expires-in.12-hours"></option>
<option value="86400" t="expires-in.1-day"></option>
<option value="604800" t="expires-in.1-week"></option>
<option value="1209600" t="expires-in.2-weeks"></option>
</select>
</label>
</fieldset>
<button type="submit" id="enter-password" t="generate-link"></button> <article id="not-found" class="hidden">
</form> <header t="password-not-found"></header>
</article>
</main>
<dialog id="link"> <p t="not-found-reasons"></p>
<article>
<header t="share-link"></header>
<fieldset role="group">
<input readonly id="link"/>
<button t="copy" id="link-copy"></button>
</fieldset>
<footer>
<button class="secondary" id="link-close" t="close"></button>
</footer>
</article>
</dialog>
<dialog id="not-found"> <button id="not-found-ok" t="ok"></button>
<article> </article>
<header t="not-found"></header>
<p t="not-found-reason"></p>
<footer>
<button t="close" id="not-found-close"></button>
</footer>
</article>
</dialog>
<dialog id="confirm"> <article id="confirm" class="hidden">
<article> <header t="reveal-password"></header>
<header t="reveal-password"></header>
<p t="reveal-password-once"></p>
<footer>
<button class="secondary" t="close" id="confirm-close"></button>
<button t="ok" id="confirm-ok"></button>
</footer>
</article>
</dialog>
<dialog id="view"> <p t="reveal-password-once"></p>
<article>
<header t="password"></header>
<textarea readonly id="view-password"></textarea>
<footer>
<button t="close" id="view-close"></button>
</footer>
</article>
</dialog>
<dialog id="error"> <button id="confirm-no" class="secondary" t="no"></button>
<article> <button id="confirm-yes" t="yes"></button>
<header t="error"></header> </article>
<textarea readonly id="error-message"></textarea> <article id="view" class="hidden">
<p t="password"></p>
<footer> <textarea id="view-password"></textarea>
<button t="close" id="error-close"></button>
</footer> <button id="view-ok" t="ok"></button>
</article> </article>
</dialog>
</body> <article id="share" class="hidden">
</html> <header t="share-password"></header>
<form id="share-form">
<fieldset id="share-fieldset">
<label>
<span t="password"></span>
<textarea name="password" id="share-password"></textarea>
</label>
<label>
<span t="expires-in"></span>
<select name="expires-in">
<option value="3600" t="1-hour"></option>
<option value="43200" t="12-hours"></option>
<option value="86400" t="1-day" selected></option>
<option value="172800" t="2-days"></option>
<option value="604800" t="1-week"></option>
<option value="1209600" t="2-weeks"></option>
</select>
</label>
<button type="submit" id="share-submit" t="share"></button>
<span>
<span t="dont-have-a-password"></span>
<a class="contrast" t="generate-one" id="share-generate"></a>
</span>
</fieldset>
</form>
</article>
</main>
<dialog id="share-dialog">
<article>
<header>
<button rel="prev" id="share-close"></button>
<p t="share-link"></p>
</header>
<fieldset role="group">
<input type="text" readonly id="share-link">
<button id="share-copy" t="copy"></button>
</fieldset>
</article>
</dialog>
<dialog id="error">
<article>
<header>
<button rel="prev" id="error-close"></button>
<p t="error"></p>
</header>
<p id="error-error"></p>
</article>
</dialog>
</body>
</html>

View file

@ -1,54 +0,0 @@
/** Upload a password to the server
* @param {string} password Encryptes password
* @param {number} expiresIn Number of seconds in which the password will expire
* @returns {Promise<string>}
*/
async function uploadPassword(password, expiresIn) {
const res = await fetch("/api/password", {
method: "POST",
body: JSON.stringify({
password: password,
"expires-in": expiresIn,
}),
});
if (!res.ok) {
const msg = await res.text();
throw new Error(`Failed to upload password: ${res.status}: ${msg}`);
}
const json = await res.json();
return json.id;
}
/** Check if the server knows about the password
* @param {string} id Password ID
* @returns {Promise<bool>}
*/
async function hasPassword(id) {
const res = await fetch(`/api/password/${id}`, {
method: "HEAD",
});
if (res.status == 404) {
return false;
}
if (res.status == 204) {
return true;
}
throw new Error(`Failed to check if password exists: ${res.status}: ${msg}`);
}
/** Get a password from the server
* @param {string} id Password ID
* @returns {Promise<string>}
*/
async function getPassword(id) {
const res = await fetch(`/api/password/${id}`);
if (!res.ok) {
const msg = await res.text();
throw new Error(`Failed to get password: ${res.status}: ${msg}`);
}
const json = await res.json();
return json.password;
}

58
static/js/backend.js Normal file
View file

@ -0,0 +1,58 @@
class Backend {
constructor() { }
/**
* @param {string} password Encrypted password
* @param {number} expiresIn Seconds until the password will be deleted on the server
* @returns {Promise<string>} Password Id
*/
async createPassword(password, expiresIn) {
const res = await fetch("/api/password", {
method: "POST",
body: JSON.stringify({
"password": password,
"expires-in": expiresIn,
}),
})
if (res.status != 200) {
throw new Error(`Failed to upload password, got status ${res.status}`)
}
const id = await res.json()
return id
}
/**
* @param {string} id Password id
* @returns {Promise<bool>} If the password exists
*/
async hasPassword(id) {
const res = await fetch(`/api/password/${id}`, {
method: "HEAD",
})
switch (res.status) {
case 204:
return true
case 404:
return false
default:
throw new Error(`Failed to check if password exists: ${res.status}: ${msg}`)
}
}
/**
* @param {string} id Password id
* @returns {Promise<string>} Encrypted password
*/
async getPassword(id) {
const res = await fetch(`/api/password/${id}`)
if (!res.ok) {
const msg = await res.text()
throw new Error(`Failed to get password: ${res.status}: ${msg}`)
}
const password = await res.json()
return password
}
}

View file

@ -1,108 +1,112 @@
const ALGORITHM_NAME = "AES-GCM"; class PasswordCrypto {
const ALGORITHM_LENGTH = 256;
const IV_LENGTH = 16;
const TEXT_ENCODER = new TextEncoder(); #encoder
const TEXT_DECODER = new TextDecoder(); #decoder
/** Coverts an ArrayBuffer into a Base64 encoded string #algorithmName
* @param {ArrayBuffer} buffer #algorithmLength
* @returns {string} #ivLength
*/
function bufferToBase64(buffer) { constructor() {
let binary = ""; this.#encoder = new TextEncoder()
const bytes = new Uint8Array(buffer); this.#decoder = new TextDecoder()
const len = bytes.byteLength;
for (let i = 0; i < len; i++) { this.#algorithmName = "AES-GCM"
binary += String.fromCharCode(bytes[i]); this.#algorithmLength = 256
this.#ivLength = 16
} }
return btoa(binary); /**
} * @param {string} buffer
* @returns {string}
*/
#bufferToBase64(buffer) {
let binary = ""
const bytes = new Uint8Array(buffer)
const len = bytes.byteLength
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
/** Converts a Base64 encoded string into an ArrayBuffer return btoa(binary)
* @param {string} base64
* @returns {ArrayBuffer}
*/
function base64ToBuffer(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
} }
return bytes.buffer; /**
} * @param {string} base64
* @returns {string}
/** Generates an key and encrypts "password" using it */
* @param {Promise<string>} password #base64ToBuffer(base64) {
*/ const binaryString = atob(base64)
async function encryptPassword(password) { const len = binaryString.length
const iv = new Uint8Array(IV_LENGTH); const bytes = new Uint8Array(len)
window.crypto.getRandomValues(iv); for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
const key = await window.crypto.subtle.generateKey( }
{
name: ALGORITHM_NAME, return bytes.buffer
length: ALGORITHM_LENGTH, }
},
true, /**
["encrypt", "decrypt"], * @param {string} password Plain text password
); * @returns {Promise<{ password: string, key: string, iv: string }>} Encrypted password
*/
const encryptedPassword = await window.crypto.subtle.encrypt( async encryptPassword(password) {
{ const iv = new Uint8Array(this.#ivLength)
name: ALGORITHM_NAME, window.crypto.getRandomValues(iv)
iv: iv,
}, const key = await window.crypto.subtle.generateKey(
key, {
TEXT_ENCODER.encode(password), name: this.#algorithmName,
); length: this.#algorithmLength,
},
const exportedKey = await window.crypto.subtle.exportKey("raw", key); true,
["encrypt", "decrypt"],
const encodedPassword = bufferToBase64(encryptedPassword); )
const encodedKey = bufferToBase64(exportedKey); const rawKey = await window.crypto.subtle.exportKey("raw", key)
const encodedIv = bufferToBase64(iv.buffer);
const encrypted = await window.crypto.subtle.encrypt(
return { {
password: encodedPassword, name: this.#algorithmName,
key: encodedKey, iv: iv,
iv: encodedIv, },
}; key,
} this.#encoder.encode(password),
)
/** Decodes to key and iv and decryptes the password
* @param {string} password return {
* @param {string} key password: this.#bufferToBase64(encrypted),
* @param {string} iv key: this.#bufferToBase64(rawKey),
* @returns {Promise<string>} iv: this.#bufferToBase64(iv.buffer),
*/ }
async function decryptPassword(password, key, iv) { }
const decodedPassword = base64ToBuffer(password);
const decodedKey = base64ToBuffer(key); /**
const decodedIv = base64ToBuffer(iv); * @param {string} password Encrypted password
* @param {string} key
const cryptoKey = await window.crypto.subtle.importKey( * @param {string} iv
"raw", * @returns {Promise<string>} Plain text password
decodedKey, */
{ async decryptPassword(password, key, iv) {
name: ALGORITHM_NAME, const ckey = await window.crypto.subtle.importKey(
length: ALGORITHM_LENGTH, "raw",
}, this.#base64ToBuffer(key),
true, {
["encrypt", "decrypt"], name: this.#algorithmName,
); length: this.#algorithmLength,
},
const decryptedPassword = await window.crypto.subtle.decrypt( true,
{ ["encrypt", "decrypt"],
name: ALGORITHM_NAME, )
iv: decodedIv,
}, const dpassword = await window.crypto.subtle.decrypt(
cryptoKey, {
decodedPassword, name: this.#algorithmName,
); iv: this.#base64ToBuffer(iv),
},
return TEXT_DECODER.decode(decryptedPassword); ckey,
this.#base64ToBuffer(password),
)
return this.#decoder.decode(dpassword)
}
} }

View file

@ -1,71 +1,79 @@
const LANGUAGE_KEY = "language"; class Language {
const languages = {};
let language = "en";
/** #storageKey
* @param {string} lang #languages
*/ #language
async function setLanguage(lang) { #errorHandler
if (languages[lang] == undefined) { #defaultLang
const res = await fetch(`/lang/${lang}.json`);
const json = await res.json();
languages[lang] = json; constructor(errorHandler, defaultLang) {
} this.#storageKey = "language"
this.#languages = new Map()
this.#errorHandler = errorHandler
this.#defaultLang = defaultLang
language = lang; const setLanguage = localStorage.getItem(this.#storageKey)
translatePage(); if (setLanguage == null) {
localStorage.setItem(LANGUAGE_KEY, lang); this.setLanguage(this.#defaultLang)
} this.#language = this.#defaultLang
return
/** Translates a key
* @param {string} key
* @param {any} args
* @returns {string}
*/
function translate(key, ...args) {
const bundle = languages[language];
let value = bundle[key];
if (value == undefined) {
value = key;
}
for (let i = 0; i < args.length; i++) {
value = value.replaceAll(`{${i}}`, args[i]);
}
return value;
}
function translatePage() {
const elements = document.getElementsByTagName("*");
for (const element of elements) {
const key = element.getAttribute("t");
if (key == null) {
continue;
} }
element.innerHTML = translate(key); this.#language = setLanguage
} this.setLanguage(this.#language)
}
function initLanguageButtons() {
const buttons = document.querySelectorAll("li.select-language");
for (const button of buttons) {
button.addEventListener("click", async (ev) => {
const self = ev.target;
const lang = self.getAttribute("data-lang");
await setLanguage(lang);
});
}
}
window.addEventListener("load", async () => {
let lang = localStorage.getItem(LANGUAGE_KEY);
if (lang == null) {
lang = "en";
} }
await setLanguage(lang); async #fetchLanguage(lang) {
initLanguageButtons(); const cached = this.#languages.get(lang)
}); if (cached != null) {
return cached
}
const f = await fetch(`/lang/${lang}.json`)
const fetched = await f.json()
this.#languages.set(lang, fetched)
return fetched
}
#translate(key, ...args) {
let bundle = this.#languages.get(this.#language)
if (bundle == null) {
bundle = {}
}
let value = bundle[key];
if (value == undefined) {
value = key
}
for (let i = 0; i < args.length; i++) {
value = value.replaceAll(`{${i}}`, args[i])
}
return value
}
#translatePage() {
const elements = document.getElementsByTagName("*")
for (const element of elements) {
const key = element.getAttribute("t")
if (key == null) {
continue
}
element.innerHTML = this.#translate(key)
}
}
setLanguage(lang) {
this.#fetchLanguage(lang)
.then(() => {
this.#language = lang
localStorage.setItem(this.#storageKey, lang)
this.#translatePage()
})
.catch((err) => this.#errorHandler(err))
}
}

View file

@ -1,136 +1,200 @@
function initErrorDialog() { /**
const dialog = document.querySelector("dialog#error"); * @param {(e: *) => void} errorHandler
const message = document.querySelector("textarea#error-message"); * @param {Backend} backend
const close = document.querySelector("button#error-close"); * @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")
close.addEventListener("click", () => dialog.close()); form.addEventListener("submit", async (e) => {
const data = new FormData(e.target)
return (err) => { e.preventDefault()
message.value = err.toString();
dialog.showModal();
};
}
function initEnterPassword(errorDialog) {
const form = document.querySelector("form#enter-password");
const fieldset = document.querySelector("fieldset#enter-password");
const submit = document.querySelector("button#enter-password");
const dialog = document.querySelector("dialog#link");
const link = document.querySelector("input#link");
const copy = document.querySelector("button#link-copy");
const close = document.querySelector("button#link-close");
form.addEventListener("submit", async (ev) => {
const data = new FormData(ev.target);
ev.preventDefault();
try { try {
fieldset.disabled = true; fieldset.disabled = true
submit.ariaBusy = "true"; submit.ariaBusy = "true"
const password = await encryptPassword(data.get("password")); const encrypted = await crypto.encryptPassword(data.get("password"))
const id = await backend.createPassword(
const id = await uploadPassword( encrypted.password,
password.password,
parseInt(data.get("expires-in")), parseInt(data.get("expires-in")),
); )
const url = new URL(window.location.toString()); const url = new URL(window.location)
url.hash = [id, password.key, password.iv].join(":"); url.hash = [id, encrypted.key, encrypted.iv].join(urlSplit)
url.search = ""
link.value = url.toString(); link.value = url.toString()
dialog.showModal(); dialog.showModal()
} catch (error) { } catch (e) {
errorDialog(error); errorHandler(e)
} finally { } finally {
fieldset.disabled = false; fieldset.disabled = false
submit.ariaBusy = "false"; submit.ariaBusy = "false"
ev.target.reset();
} }
}); })
dialog.addEventListener("close", (ev) => { copy.addEventListener("click", () => {
this.link.value = ""; link.select()
}); link.setSelectionRange(0, 99999)
navigator.clipboard.writeText(link.value)
copy.addEventListener("click", (ev) => { })
link.select();
link.setSelectionRange(0, 99999);
navigator.clipboard.writeText(link.value);
});
close.addEventListener("click", (ev) => {
dialog.close();
});
}
async function confirmViewPassword(errorDialog) {
const dialog = document.querySelector("dialog#confirm");
const close = document.querySelector("button#confirm-close");
const ok = document.querySelector("button#confirm-ok");
const notFoundDialog = document.querySelector("dialog#not-found");
const notFoundClose = document.querySelector("button#not-found-close");
let hash = window.location.hash;
hash = hash.substring(1);
if (hash.trim() == "") {
return;
}
const [id, key, iv] = hash.split(":");
const has = await hasPassword(id);
if (!has) {
notFoundClose.addEventListener("click", () => notFoundDialog.close());
notFoundDialog.addEventListener("close", () => (window.location.hash = ""));
notFoundDialog.showModal();
return;
}
dialog.addEventListener("close", () => (window.location.hash = ""));
close.addEventListener("click", () => { close.addEventListener("click", () => {
dialog.close(); dialog.close()
}); })
ok.addEventListener("click", async () => { generate.addEventListener("click", () => {
try { let result = ""
close.disabled = true; const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
ok.disabled = true; for (let i = 0; i < 12; i++) {
ok.ariaBusy = "true"; result += charset.charAt(Math.floor(Math.random() * charset.length))
const encryptedPassword = await getPassword(id);
const password = await decryptPassword(encryptedPassword, key, iv);
viewPassword(password);
} catch (error) {
errorDialog(error);
} finally {
close.disabled = false;
ok.disabled = false;
ok.ariaBusy = "false";
dialog.close();
} }
});
dialog.showModal(); password.value = result
})
} }
async function viewPassword(password) { /**
const dialog = document.querySelector("dialog#view"); * @param {(e: *) => void} errorHandler
const viewPassword = document.querySelector("textarea#view-password"); * @param {Backend} backend
const close = document.querySelector("button#view-close"); * @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")
close.addEventListener("click", () => dialog.close()); const hash = window.location.hash
dialog.addEventListener("close", () => (window.location.hash = ""), 0); if (hash == "" || hash == "#") {
share.classList.remove(hidden)
return
}
viewPassword.value = password; const raw = hash.substring(1)
dialog.showModal(); 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()
} }
window.addEventListener("load", () => { /**
const errorDialog = initErrorDialog(); * @param {Language} language
initEnterPassword(errorDialog); */
confirmViewPassword(errorDialog); 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())

View file

@ -1,22 +1,26 @@
{ {
"title": "PassED", "share-password": "Password teilen",
"source-code": "Quellcode", "share": "Teilen",
"language": "Sprache",
"enter-password": "Passwort eingeben",
"password": "Passwort", "password": "Passwort",
"expires-in": "Ablaufzeit", "expires-in": "Ablaufzeit",
"expires-in.1-hour": "1 Stunde", "1-hour": "1 Stunde",
"expires-in.12-hours": "12 Stunden", "12-hours": "12 Stunden",
"expires-in.1-day": "1 Tag", "1-day": "1 Tag",
"expires-in.1-week": "1 Woche", "2-days": "2 Tage",
"expires-in.2-weeks": "2 Wochen", "1-week": "1 Woche",
"generate-link": "Link erstellen", "2-weeks": "2 Wochen",
"share-link": "Link teilen", "share-link": "Link teilen",
"not-found": "Passwort wurde nicht gefunden",
"not-found-reason": "Das von Ihnen angefragte Passwort ist vielleicht abgelaufen, wurde bereits angeschaut oder hat noch nie existiert.",
"reveal-password": "Passwort anschauen",
"reveal-password-once": "Sie können sich das Passwort nur einmal anschauen.",
"copy": "Kopieren", "copy": "Kopieren",
"close": "Schließen", "error": "Fehler",
"ok": "OK" "ok": "OK",
"yes": "Ja",
"no": "Nein",
"reveal-password": "Password anzeigen",
"reveal-password-once": "Sie können sich das Password nur ein mal anschauen.",
"not-found": "Passwort nicht gefunden",
"not-found-reasons": "Das von ihnen angefragte Passwort ist evtl abgelaufen, wurde bereits angesehen oder hat noch nie existiert.",
"source": "Quellcode",
"language": "Sprace",
"dont-have-a-password": "Haben Sie kein Passwort?",
"generate-one": "Generieren Sie eins."
} }

View file

@ -1,23 +1,26 @@
{ {
"title": "PassED", "share-password": "Share password",
"source-code": "Source", "share": "Share",
"language": "Language",
"enter-password": "Enter Password",
"password": "Password", "password": "Password",
"expires-in": "Expires in", "expires-in": "Expires in",
"expires-in.1-hour": "1 Hour", "1-hour": "1 hour",
"expires-in.12-hours": "2 Hours", "12-hours": "12 hours",
"expires-in.1-day": "1 Day", "1-day": "1 day",
"expires-in.1-week": "1 Week", "2-days": "2 days",
"expires-in.2-weeks": "2 Weeks", "1-week": "1 week",
"generate-link": "Generate Link", "2-weeks": "2 weeks",
"share-link": "Share Link", "share-link": "Share link",
"not-found": "Password not found",
"not-found-reason": "The password you requested may have expired, been viewed before or never even existed in the first place.",
"reveal-password": "Reveal Password",
"reveal-password-once": "You may only reveal the password once",
"error": "Error",
"copy": "Copy", "copy": "Copy",
"close": "Close", "error": "Error",
"ok": "OK" "ok": "OK",
"yes": "Yes",
"no": "No",
"reveal-password": "Reveal password",
"reveal-password-once": "You may only reveal the password once.",
"not-found": "Password not found",
"not-found-reasons": "The password you requested may have expired, been viewed before or never even existed in the first place.",
"source": "Source",
"language": "Language",
"dont-have-a-password": "Don't have a password?",
"generate-one": "Generate one."
} }

View file

@ -1,22 +0,0 @@
{
"title": "PassED",
"source-code": "Fuente",
"language": "Idioma",
"enter-password": "Introducir Contraseña",
"password": "Contraseña",
"expires-in": "Expira en",
"expires-in.1-hour": "1 Hora",
"expires-in.12-hours": "2 Horas",
"expires-in.1-day": "1 Día",
"expires-in.1-week": "1 Semana",
"expires-in.2-weeks": "2 Semanas",
"generate-link": "Generar Enlace",
"share-link": "Compartir Enlace",
"not-found": "Contraseña no encontrada",
"not-found-reason": "La contraseña que solicitaste puede haber expirado, haber sido vista antes o nunca haber existido en primer lugar.",
"reveal-password": "Revelar Contraseña",
"reveal-password-once": "Solo puedes revelar la contraseña una vez",
"copy": "Copiar",
"close": "Cerrar",
"ok": "OK"
}

View file

@ -1,22 +0,0 @@
{
"title": "PassED",
"source-code": "Source",
"language": "Langue",
"enter-password": "Entrer le mot de passe",
"password": "Mot de passe",
"expires-in": "Expire dans",
"expires-in.1-hour": "1 Heure",
"expires-in.12-hours": "2 Heures",
"expires-in.1-day": "1 Jour",
"expires-in.1-week": "1 Semaine",
"expires-in.2-weeks": "2 Semaines",
"generate-link": "Générer le lien",
"share-link": "Partager le lien",
"not-found": "Mot de passe non trouvé",
"not-found-reason": "Le mot de passe que vous avez demandé a peut-être expiré, a été consulté auparavant ou n'a jamais existé.",
"reveal-password": "Révéler le mot de passe",
"reveal-password-once": "Vous ne pouvez révéler le mot de passe qu'une seule fois",
"copy": "Copier",
"close": "Fermer",
"ok": "OK"
}

View file

@ -1,22 +0,0 @@
{
"title": "PassED",
"source-code": "Bron",
"language": "Taal",
"enter-password": "Wachtwoord invoeren",
"password": "Wachtwoord",
"expires-in": "Expires in",
"expires-in.1-hour": "1 uur",
"expires-in.12-hours": "2 uur",
"expires-in.1-day": "1 dag",
"expires-in.1-week": "1 week",
"expires-in.2-weeks": "2 weken",
"generate-link": "Maak link",
"share-link": "Deel Link",
"not-found": "Wachtwoord niet gevonden",
"not-found-reason": "Het door u aangevraagde wachtwoord is mogelijk verlopen, eerder bekeken of heeft zelfs nooit bestaan.",
"reveal-password": "Toon wachtwoord",
"reveal-password-once": "U mag het wachtwoord slechts één keer onthullen.",
"copy": "Kopieer",
"close": "Sluit",
"ok": "OK"
}

View file

@ -1,22 +0,0 @@
{
"title": "PassED",
"source-code": "Sursă",
"language": "Limbă",
"enter-password": "Introduceți parolă",
"password": "Parolă",
"expires-in": "Expiră în",
"expires-in.1-hour": "O oră",
"expires-in.12-hours": "2 ore",
"expires-in.1-day": "O zi",
"expires-in.1-week": "O săptămână",
"expires-in.2-weeks": "2 săptămâni",
"generate-link": "Generați link",
"share-link": "Distribuiți link",
"not-found": "Parola nu poate fi găsită",
"not-found-reason": "Parola cerută este posibil să fi expirat, fost folosită, sau să nu existe.",
"reveal-password": "Dezvăluiți parola",
"reveal-password-once": "Parola nu poate fi dezvăluită decât o dată",
"copy": "Copiază",
"close": "Inchide",
"ok": "OK"
}