Compare commits
5 commits
78ea484a71
...
55ce44aebf
Author | SHA1 | Date | |
---|---|---|---|
|
55ce44aebf | ||
|
43b73bbe5f | ||
|
c59e56fba7 | ||
|
dd3b797ec2 | ||
|
36bd59ef02 |
7 changed files with 90 additions and 19 deletions
|
@ -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
8
src/const.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const allowedColorModes = [
|
||||||
|
'light',
|
||||||
|
'dark',
|
||||||
|
];
|
||||||
|
export const allowedLocales = [
|
||||||
|
'en',
|
||||||
|
'fr',
|
||||||
|
];
|
|
@ -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.",
|
||||||
|
|
|
@ -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
19
src/locales_utils.js
Normal 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;
|
||||||
|
};
|
11
src/main.js
11
src/main.js
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
resetErrorMessage();
|
||||||
|
var hasError = false;
|
||||||
|
var key = null;
|
||||||
|
var accountId = null;
|
||||||
|
if (separator.value.length != 1) {
|
||||||
|
hasError = setErrorMessage('addAccount.error.invalidSeparator', separatorErrorMessageId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (separator.value.length != 1) {
|
|
||||||
throw new Error('addAccount.error.invalidSeparator');
|
|
||||||
}
|
|
||||||
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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue