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
}
// Clear storage at regular intervals
go func() {
ticker := time.Tick(time.Duration(clearInterval) * time.Second)
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
}
resBody := struct {
Id string `json:"id"`
}{
Id: id,
}
err = json.NewEncoder(res).Encode(&resBody)
err = json.NewEncoder(res).Encode(&id)
if err != nil {
http.Error(res, "", http.StatusInternalServerError)
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>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!--<link rel="icon" href="favicon.png" />-->
<title>PassED</title>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/api.js"></script>
<script src="/js/crypto.js"></script>
<script src="/js/main.js"></script>
</head>
<nav class="container">
<ul>
<li>
<strong t="title"></strong>
</li>
</ul>
<script src="/js/lang.js"></script>
<script src="/js/backend.js"></script>
<script src="/js/crypto.js"></script>
<script src="/js/main.js"></script>
<ul>
<li>
<a href="https://git.1e99.eu/1e99/passed/" t="source-code"></a>
</li>
<title>PassED</title>
</head>
<li>
<details class="dropdown">
<summary t="language"></summary>
<ul dir="rtl">
<li class="select-language" data-lang="de">Deutsch</li>
<li class="select-language" data-lang="en">English</li>
<li class="select-language" data-lang="es">Español</li>
<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>
<body>
<nav class="container">
<ul>
<li>
<strong>PassED</strong>
</li>
</ul>
<main class="container">
<article>
<header t="enter-password"></header>
<ul>
<li>
<a href="https://git.1e99.eu/1e99/passed" t="source"></a>
</li>
<form id="enter-password">
<fieldset id="enter-password">
<label>
<span t="password"></span>
<textarea name="password"></textarea>
</label>
<li>
<details class="dropdown">
<summary t="language"></summary>
<ul dir="rtl">
<li class="select-lang" data-lang="de">Deutsch</li>
<li class="select-lang" data-lang="en">English</li>
</ul>
</details>
</li>
</ul>
</nav>
<label>
<span t="expires-in"></span>
<select name="expires-in">
<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>
<main class="container">
<article id="loading" class="hidden" aria-busy="true">
</article>
<button type="submit" id="enter-password" t="generate-link"></button>
</form>
</article>
</main>
<article id="not-found" class="hidden">
<header t="password-not-found"></header>
<dialog id="link">
<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>
<p t="not-found-reasons"></p>
<dialog id="not-found">
<article>
<header t="not-found"></header>
<p t="not-found-reason"></p>
<footer>
<button t="close" id="not-found-close"></button>
</footer>
</article>
</dialog>
<button id="not-found-ok" t="ok"></button>
</article>
<dialog id="confirm">
<article>
<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>
<article id="confirm" class="hidden">
<header t="reveal-password"></header>
<dialog id="view">
<article>
<header t="password"></header>
<textarea readonly id="view-password"></textarea>
<footer>
<button t="close" id="view-close"></button>
</footer>
</article>
</dialog>
<p t="reveal-password-once"></p>
<dialog id="error">
<article>
<header t="error"></header>
<button id="confirm-no" class="secondary" t="no"></button>
<button id="confirm-yes" t="yes"></button>
</article>
<textarea readonly id="error-message"></textarea>
<article id="view" class="hidden">
<p t="password"></p>
<textarea id="view-password"></textarea>
<button id="view-ok" t="ok"></button>
</article>
<article id="share" class="hidden">
<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>
<footer>
<button t="close" id="error-close"></button>
</footer>
</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";
const ALGORITHM_LENGTH = 256;
const IV_LENGTH = 16;
class PasswordCrypto {
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();
#encoder
#decoder
/** Coverts an ArrayBuffer into a Base64 encoded string
* @param {ArrayBuffer} buffer
* @returns {string}
*/
function 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]);
#algorithmName
#algorithmLength
#ivLength
constructor() {
this.#encoder = new TextEncoder()
this.#decoder = new TextDecoder()
this.#algorithmName = "AES-GCM"
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
* @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 btoa(binary)
}
return bytes.buffer;
}
/** Generates an key and encrypts "password" using it
* @param {Promise<string>} password
*/
async function encryptPassword(password) {
const iv = new Uint8Array(IV_LENGTH);
window.crypto.getRandomValues(iv);
const key = await window.crypto.subtle.generateKey(
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const encryptedPassword = await window.crypto.subtle.encrypt(
{
name: ALGORITHM_NAME,
iv: iv,
},
key,
TEXT_ENCODER.encode(password),
);
const exportedKey = await window.crypto.subtle.exportKey("raw", key);
const encodedPassword = bufferToBase64(encryptedPassword);
const encodedKey = bufferToBase64(exportedKey);
const encodedIv = bufferToBase64(iv.buffer);
return {
password: encodedPassword,
key: encodedKey,
iv: encodedIv,
};
}
/** Decodes to key and iv and decryptes the password
* @param {string} password
* @param {string} key
* @param {string} iv
* @returns {Promise<string>}
*/
async function decryptPassword(password, key, iv) {
const decodedPassword = base64ToBuffer(password);
const decodedKey = base64ToBuffer(key);
const decodedIv = base64ToBuffer(iv);
const cryptoKey = await window.crypto.subtle.importKey(
"raw",
decodedKey,
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const decryptedPassword = await window.crypto.subtle.decrypt(
{
name: ALGORITHM_NAME,
iv: decodedIv,
},
cryptoKey,
decodedPassword,
);
return TEXT_DECODER.decode(decryptedPassword);
/**
* @param {string} base64
* @returns {string}
*/
#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} password Plain text password
* @returns {Promise<{ password: string, key: string, iv: string }>} Encrypted password
*/
async encryptPassword(password) {
const iv = new Uint8Array(this.#ivLength)
window.crypto.getRandomValues(iv)
const key = await window.crypto.subtle.generateKey(
{
name: this.#algorithmName,
length: this.#algorithmLength,
},
true,
["encrypt", "decrypt"],
)
const rawKey = await window.crypto.subtle.exportKey("raw", key)
const encrypted = await window.crypto.subtle.encrypt(
{
name: this.#algorithmName,
iv: iv,
},
key,
this.#encoder.encode(password),
)
return {
password: this.#bufferToBase64(encrypted),
key: this.#bufferToBase64(rawKey),
iv: this.#bufferToBase64(iv.buffer),
}
}
/**
* @param {string} password Encrypted password
* @param {string} key
* @param {string} iv
* @returns {Promise<string>} Plain text password
*/
async decryptPassword(password, key, iv) {
const ckey = await window.crypto.subtle.importKey(
"raw",
this.#base64ToBuffer(key),
{
name: this.#algorithmName,
length: this.#algorithmLength,
},
true,
["encrypt", "decrypt"],
)
const dpassword = await window.crypto.subtle.decrypt(
{
name: this.#algorithmName,
iv: this.#base64ToBuffer(iv),
},
ckey,
this.#base64ToBuffer(password),
)
return this.#decoder.decode(dpassword)
}
}

View file

@ -1,71 +1,79 @@
const LANGUAGE_KEY = "language";
const languages = {};
let language = "en";
class Language {
/**
* @param {string} lang
*/
async function setLanguage(lang) {
if (languages[lang] == undefined) {
const res = await fetch(`/lang/${lang}.json`);
const json = await res.json();
#storageKey
#languages
#language
#errorHandler
#defaultLang
languages[lang] = json;
}
constructor(errorHandler, defaultLang) {
this.#storageKey = "language"
this.#languages = new Map()
this.#errorHandler = errorHandler
this.#defaultLang = defaultLang
language = lang;
translatePage();
localStorage.setItem(LANGUAGE_KEY, lang);
}
/** 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;
const setLanguage = localStorage.getItem(this.#storageKey)
if (setLanguage == null) {
this.setLanguage(this.#defaultLang)
this.#language = this.#defaultLang
return
}
element.innerHTML = translate(key);
this.#language = setLanguage
this.setLanguage(this.#language)
}
async #fetchLanguage(lang) {
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))
}
}
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);
initLanguageButtons();
});

View file

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

View file

@ -1,22 +1,26 @@
{
"title": "PassED",
"source-code": "Quellcode",
"language": "Sprache",
"enter-password": "Passwort eingeben",
"share-password": "Password teilen",
"share": "Teilen",
"password": "Passwort",
"expires-in": "Ablaufzeit",
"expires-in.1-hour": "1 Stunde",
"expires-in.12-hours": "12 Stunden",
"expires-in.1-day": "1 Tag",
"expires-in.1-week": "1 Woche",
"expires-in.2-weeks": "2 Wochen",
"generate-link": "Link erstellen",
"1-hour": "1 Stunde",
"12-hours": "12 Stunden",
"1-day": "1 Tag",
"2-days": "2 Tage",
"1-week": "1 Woche",
"2-weeks": "2 Wochen",
"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",
"close": "Schließen",
"ok": "OK"
"error": "Fehler",
"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",
"source-code": "Source",
"language": "Language",
"enter-password": "Enter Password",
"share-password": "Share password",
"share": "Share",
"password": "Password",
"expires-in": "Expires in",
"expires-in.1-hour": "1 Hour",
"expires-in.12-hours": "2 Hours",
"expires-in.1-day": "1 Day",
"expires-in.1-week": "1 Week",
"expires-in.2-weeks": "2 Weeks",
"generate-link": "Generate 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",
"1-hour": "1 hour",
"12-hours": "12 hours",
"1-day": "1 day",
"2-days": "2 days",
"1-week": "1 week",
"2-weeks": "2 weeks",
"share-link": "Share link",
"copy": "Copy",
"close": "Close",
"ok": "OK"
"error": "Error",
"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"
}