Compare commits

..

5 commits

Author SHA1 Message Date
Rodolphe Bréard
55ce44aebf Forbid to add the same account twice 2023-09-26 12:15:44 +02:00
Rodolphe Bréard
43b73bbe5f Use Bootstrap's form validation style 2023-09-26 11:58:33 +02:00
Rodolphe Bréard
c59e56fba7 Fix a typo 2023-09-26 11:13:26 +02:00
Rodolphe Bréard
dd3b797ec2 Use the browser's preferred language if available 2023-09-26 10:58:29 +02:00
Rodolphe Bréard
36bd59ef02 Automatically correct invalid values for preferences 2023-09-26 10:31:09 +02:00
7 changed files with 90 additions and 19 deletions

View file

@ -17,10 +17,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Possibility to set a default account - Possibility to set a default account
- Dark mode - Dark mode
- Use the browser's preferred language if available
### Changed ### Changed
- The style has been entirely reworked using Bootstrap instead of Bulma - The style has been entirely reworked using Bootstrap instead of Bulma
- It is now impossible to include the separator in the dedicated name - It is now impossible to include the separator in the dedicated name
- When adding a new account, error messages are displayed alongside each affected elements whenever possible
## Fixed
- Invalid preferences are now automatically corrected
- It is now impossible to add the same account twice
## [0.3.0] - 2023-08-25 ## [0.3.0] - 2023-08-25

8
src/const.js Normal file
View file

@ -0,0 +1,8 @@
export const allowedColorModes = [
'light',
'dark',
];
export const allowedLocales = [
'en',
'fr',
];

View file

@ -35,6 +35,7 @@
"addAccount": "Add account", "addAccount": "Add account",
"cancel": "@:invariants.controls.cancel", "cancel": "@:invariants.controls.cancel",
"error": { "error": {
"accountAlreadyExists": "You already have an account on this domain that uses this local part.",
"invalidBase64": "The key must be a valid base64 string.", "invalidBase64": "The key must be a valid base64 string.",
"invalidKeyLength": "The key's length must be either 128 bits (16 bytes) or 256 bits (32 bytes).", "invalidKeyLength": "The key's length must be either 128 bits (16 bytes) or 256 bits (32 bytes).",
"invalidSeparator": "The separator must be a single character.", "invalidSeparator": "The separator must be a single character.",

View file

@ -35,9 +35,10 @@
"addAccount": "Ajouter", "addAccount": "Ajouter",
"cancel": "@:invariants.controls.cancel", "cancel": "@:invariants.controls.cancel",
"error": { "error": {
"accountAlreadyExists": "Vous avez déjà un compte sur ce nom de domaine qui utilise cette partie locale.",
"invalidBase64": "La clé doit être une chaîne de caractère en base64.", "invalidBase64": "La clé doit être une chaîne de caractère en base64.",
"invalidKeyLength": "La longueur de la clé doit être de 128 bits (16 bytes) ou de 256 bits (32 bytes).", "invalidKeyLength": "La longueur de la clé doit être de 128 bits (16 bytes) ou de 256 bits (32 bytes).",
"invalidSeparator": "La séparateur doit être un unique caractère.", "invalidSeparator": "Le séparateur doit être un unique caractère.",
"cameraNotAllowed": "L'accès à la caméra n'a pas été autorisé.", "cameraNotAllowed": "L'accès à la caméra n'a pas été autorisé.",
"cameraNotFound": "Aucune caméra détectée.", "cameraNotFound": "Aucune caméra détectée.",
"cameraInsecureContext": "Impossible d'accéder à la caméra depuis une liaison non-sécurisée.", "cameraInsecureContext": "Impossible d'accéder à la caméra depuis une liaison non-sécurisée.",

19
src/locales_utils.js Normal file
View file

@ -0,0 +1,19 @@
import { allowedLocales } from './const';
const fallBackValue = 'en';
const getShortLanguageCode = (language) => {
language = language.split('-')[0];
language = language.split('_')[0];
return language;
};
export const getDefaultLocale = () => {
for (const lang of navigator.languages) {
const lang_short = getShortLanguageCode(lang);
if (allowedLocales.includes(lang_short)) {
return lang_short;
}
}
return fallBackValue;
};

View file

@ -1,5 +1,7 @@
import './assets/main.sass'; import './assets/main.sass';
import { allowedColorModes, allowedLocales } from './const';
import { getDefaultLocale } from './locales_utils';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
@ -7,19 +9,22 @@ import App from './App.vue';
import router from './router'; import router from './router';
import messages from '@intlify/unplugin-vue-i18n/messages'; import messages from '@intlify/unplugin-vue-i18n/messages';
const setGlobalAttribute = (attrName, storageName, defaultValue) => { const setGlobalAttribute = (attrName, storageName, defaultValue, allowedValues) => {
const stored_value = useStorage(storageName, ''); const stored_value = useStorage(storageName, '');
if (!stored_value.value) { if (!stored_value.value) {
stored_value.value = defaultValue; stored_value.value = defaultValue;
} }
document.documentElement.setAttribute(attrName, stored_value.value); document.documentElement.setAttribute(attrName, stored_value.value);
if (!allowedValues.includes(stored_value.value)) {
stored_value.value = defaultValue;
}
return { return {
'stored': stored_value, 'stored': stored_value,
'defaultValue': defaultValue, 'defaultValue': defaultValue,
}; };
}; };
const locale = setGlobalAttribute('lang', 'sake-locale', 'en'); const locale = setGlobalAttribute('lang', 'sake-locale', getDefaultLocale(), allowedLocales);
const colorMode = setGlobalAttribute('data-bs-theme', 'sake-color-mode', 'light'); const colorMode = setGlobalAttribute('data-bs-theme', 'sake-color-mode', 'light', allowedColorModes);
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,

View file

@ -15,9 +15,13 @@ const localPart = ref('');
const separator = ref('+'); const separator = ref('+');
const domainName = ref(''); const domainName = ref('');
const privateKey = ref(''); const privateKey = ref('');
const errorMessageId = ref('');
const authorizedKeyLengths = [16, 32]; const authorizedKeyLengths = [16, 32];
const errorMessageId = ref('');
const separatorErrorMessageId = ref('');
const localPartErrorMessageId = ref('');
const addrKeyErrorMessageId = ref('');
const base64Decode = (str_b64) => { const base64Decode = (str_b64) => {
try { try {
const raw_str = atob(str_b64); const raw_str = atob(str_b64);
@ -44,18 +48,38 @@ const addDisabled = computed(() => {
}); });
const addAccount = () => { const addAccount = () => {
if (!addDisabled.value) { if (!addDisabled.value) {
try { resetErrorMessage();
var hasError = false;
var key = null;
var accountId = null;
if (separator.value.length != 1) { if (separator.value.length != 1) {
throw new Error('addAccount.error.invalidSeparator'); hasError = setErrorMessage('addAccount.error.invalidSeparator', separatorErrorMessageId);
} }
try {
if (localPart.value.includes(separator.value)) { if (localPart.value.includes(separator.value)) {
throw new Error('addAccount.error.localPartSeparator'); throw new Error('addAccount.error.localPartSeparator');
} }
const key = base64Decode(privateKey.value); accountId = `${localPart.value}@${domainName.value}`;
for (const acc of accounts.value) {
const comp = `${acc.localPart}@${acc.domain}`;
if (accountId == comp) {
throw new Error('addAccount.error.accountAlreadyExists');
}
}
} catch (e) {
console.log(e);
hasError = setErrorMessage(e.message, localPartErrorMessageId);
}
try {
key = base64Decode(privateKey.value);
if (!authorizedKeyLengths.includes(key.length)) { if (!authorizedKeyLengths.includes(key.length)) {
throw new Error('addAccount.error.invalidKeyLength'); throw new Error('addAccount.error.invalidKeyLength');
} }
const hash = sha256(`${localPart.value}@${domainName.value}`); } catch (e) {
hasError = setErrorMessage(e.message, addrKeyErrorMessageId);
}
if (!hasError && key && accountId) {
const hash = sha256(accountId);
const newAccount = { const newAccount = {
id: base32Encode(hash, 'RFC4648', { padding: false }).toLowerCase(), id: base32Encode(hash, 'RFC4648', { padding: false }).toLowerCase(),
localPart: localPart.value, localPart: localPart.value,
@ -66,8 +90,6 @@ const addAccount = () => {
}; };
accounts.value.push(newAccount); accounts.value.push(newAccount);
return toMainView(); return toMainView();
} catch (e) {
errorMessageId.value = e.message;
} }
} }
}; };
@ -119,15 +141,21 @@ const toMainView = () => {
}; };
// Error message // Error message
const setErrorMessage = (messageId) => { const setErrorMessage = (messageId, messageType) => {
if (messageId.startsWith('addAccount.error.')) { const messageIdClean = messageId.startsWith('addAccount.error.') ? messageId : 'addAccount.error.unknown';
errorMessageId.value = messageId; if (messageType) {
messageType.value = messageIdClean;
} else { } else {
errorMessageId.value = 'addAccount.error.unknown'; errorMessageId.value = messageIdClean;
} }
return true;
}; };
const resetErrorMessage = () => { const resetErrorMessage = () => {
errorMessageId.value = ''; errorMessageId.value = '';
separatorErrorMessageId.value = '';
localPartErrorMessageId.value = '';
addrKeyErrorMessageId.value = '';
}; };
</script> </script>
@ -142,11 +170,13 @@ const resetErrorMessage = () => {
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="new-addr-local-part">{{ $t("addAccount.localPart") }}</label> <label class="form-label" for="new-addr-local-part">{{ $t("addAccount.localPart") }}</label>
<input class="form-control" type="text" id="new-addr-local-part" v-model="localPart"> <input :class="{ 'form-control': true, 'is-invalid': localPartErrorMessageId}" type="text" id="new-addr-local-part" v-model="localPart">
<div class="invalid-feedback" v-if="localPartErrorMessageId">{{ $t(localPartErrorMessageId) }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="new-addr-separator">{{ $t("addAccount.separator") }}</label> <label class="form-label" for="new-addr-separator">{{ $t("addAccount.separator") }}</label>
<input class="form-control" type="text" id="new-addr-separator" v-model="separator"> <input :class="{ 'form-control': true, 'is-invalid': separatorErrorMessageId}" type="text" id="new-addr-separator" v-model="separator">
<div class="invalid-feedback" v-if="separatorErrorMessageId">{{ $t(separatorErrorMessageId) }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="new-addr-domain">{{ $t("addAccount.domainName") }}</label> <label class="form-label" for="new-addr-domain">{{ $t("addAccount.domainName") }}</label>
@ -155,8 +185,9 @@ const resetErrorMessage = () => {
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="new-addr-key">{{ $t("addAccount.privateKey") }}</label> <label class="form-label" for="new-addr-key">{{ $t("addAccount.privateKey") }}</label>
<div class="input-group"> <div class="input-group">
<input class="form-control" type="text" id="new-addr-key" v-model="privateKey"> <input :class="{ 'form-control': true, 'is-invalid': addrKeyErrorMessageId}" type="text" id="new-addr-key" v-model="privateKey">
<button class="btn btn-primary" type="button" @click="showQrCodeScanner">{{ $t("addAccount.scan") }}</button> <button class="btn btn-primary" type="button" @click="showQrCodeScanner">{{ $t("addAccount.scan") }}</button>
<div class="invalid-feedback" v-if="addrKeyErrorMessageId">{{ $t(addrKeyErrorMessageId) }}</div>
</div> </div>
</div> </div>