Compare commits

...

2 commits

Author SHA1 Message Date
Rodolphe Bréard
a199f2465f Migrate from Bulma to Bootstrap 2023-09-23 19:12:01 +02:00
Rodolphe Bréard
95e65cd005 Update links in the README 2023-09-23 14:11:25 +02:00
16 changed files with 194 additions and 170 deletions

View file

@ -12,6 +12,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- The style has been entirely reworked using Bootstrap instead of Bulma
## [0.3.0] - 2023-08-25
### Added
@ -23,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The local part cannot contain the separator
- The HTML lang attribute is now set to the appropriate language
## [0.2.0] - 2023-08-11
### Added

View file

@ -1,10 +1,10 @@
# Sub-Address KEy (SAKE) app
Web application that can be used to generate new sub-addresses as defined in the [Sub-Address KEy (SAKE) filter](https://github.com/breard-r/opensmtpd-filter-sake).
Web application that can be used to generate new sub-addresses as defined in the [Sub-Address KEy (SAKE) filter](https://git.what.tf/rodolphe/opensmtpd-filter-sake).
## Install
Download the build from [the latest released version](https://github.com/breard-r/sake-app/releases). Extract the archive and configure your web server to serve those files.
Download the build from [the latest released version](https://git.what.tf/rodolphe/sake-app/releases). Extract the archive and configure your web server to serve those files.
That's it. The final build is plain HTML/CSS/JS with a few assets, therefore there is no back-end to configure.

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<link rel="icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sub-Address KEy</title>

47
package-lock.json generated
View file

@ -10,9 +10,10 @@
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@noble/hashes": "^1.3.1",
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^10.2.1",
"base32-encode": "^2.0.0",
"bulma": "^0.9.4",
"bootstrap": "^5.3.2",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-qrcode-reader": "^5.1.0",
@ -583,6 +584,15 @@
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz",
@ -907,6 +917,24 @@
"node": ">=8"
}
},
"node_modules/bootstrap": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
"integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
@ -919,11 +947,6 @@
"node": ">=8"
}
},
"node_modules/bulma": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
"integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ=="
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -1523,9 +1546,9 @@
}
},
"node_modules/sass": {
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
"integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.68.0.tgz",
"integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -1733,9 +1756,9 @@
}
},
"node_modules/vue-router": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz",
"integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
"integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==",
"dependencies": {
"@vue/devtools-api": "^6.5.0"
},

View file

@ -15,9 +15,10 @@
},
"dependencies": {
"@noble/hashes": "^1.3.1",
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^10.2.1",
"base32-encode": "^2.0.0",
"bulma": "^0.9.4",
"bootstrap": "^5.3.2",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-qrcode-reader": "^5.1.0",

View file

@ -1,9 +1,4 @@
@charset "utf-8"
@import "node_modules/bulma/bulma"
a[href]
text-decoration: underline
.navbar-menu a[class~="navbar-item"]
text-decoration: none
@import "variables.scss"
@import "bootstrap/scss/bootstrap"

View file

@ -0,0 +1,7 @@
$container-max-widths: (
sm: 600px,
md: 700px,
lg: 800px,
xl: 820px,
xxl: 840px
);

View file

@ -0,0 +1,5 @@
<template>
<div class="d-grid gap-2 col-6 mx-auto">
<slot></slot>
</div>
</template>

View file

@ -1,11 +1,5 @@
<template>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-three-fifths">
<slot></slot>
</div>
</div>
</div>
</section>
<div class="container">
<slot></slot>
</div>
</template>

View file

@ -1,35 +1,34 @@
<script setup>
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
const menuActive = ref(false);
const toggleBurger = () => {
menuActive.value = !menuActive.value;
};
import LayoutComponent from '../components/LayoutComponent.vue';
import { Popover } from 'bootstrap';
</script>
<template>
<div class="container">
<div class="columns is-centered">
<div class="column is-three-fifths">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a role="button" class="navbar-burger" :class="{ 'is-active': menuActive }" aria-label="menu" aria-expanded="false" @click="toggleBurger">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" :class="{ 'is-active': menuActive }">
<div class="navbar-end">
<RouterLink to="/add-account" class="navbar-item">{{ $t("navbar.addAccount") }}</RouterLink>
<RouterLink to="/manage-accounts" class="navbar-item">{{ $t("navbar.manageAccounts") }}</RouterLink>
<RouterLink to="/config" class="navbar-item">{{ $t("navbar.config") }}</RouterLink>
<RouterLink to="/about" class="navbar-item">{{ $t("navbar.about") }}</RouterLink>
</div>
</div>
</nav>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavBar" aria-controls="mainNavBar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNavBar">
<LayoutComponent>
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<li class="nav-item">
<RouterLink to="/add-account" class="nav-link">{{ $t("navbar.addAccount") }}</RouterLink>
</li>
<li class="nav-item">
<RouterLink to="/manage-accounts" class="nav-link">{{ $t("navbar.manageAccounts") }}</RouterLink>
</li>
<li class="nav-item">
<RouterLink to="/config" class="nav-link">{{ $t("navbar.config") }}</RouterLink>
</li>
<li class="nav-item">
<RouterLink to="/about" class="nav-link">{{ $t("navbar.about") }}</RouterLink>
</li>
</ul>
</LayoutComponent>
</div>
</div>
</div>
</nav>
</template>

View file

@ -1,6 +1,7 @@
<script setup>
import { useRouter } from 'vue-router';
import { version } from '../../package.json';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
import ExternalLinkComponent from '../components/ExternalLinkComponent.vue';
@ -14,30 +15,28 @@ const toMainView = () => {
<template>
<LayoutComponent>
<h1 class="title is-1">{{ $t("about.title") }}</h1>
<h4 class="subtitle is-4">{{ $t("about.name") }}</h4>
<div class="block">
<i18n-t scope="global" keypath="about.version" tag="p">
<template v-slot:version>{{ version }}</template>
</i18n-t>
<i18n-t scope="global" keypath="about.license" tag="p">
<template v-slot:mit>
<ExternalLinkComponent :url="$t('about.license_mit_url')" name="MIT" />
</template>
<template v-slot:apache>
<ExternalLinkComponent :url="$t('about.license_apache_url')" name="Apache 2.0" />
</template>
</i18n-t>
<i18n-t scope="global" keypath="about.repository" tag="p">
<template v-slot:url>
<ExternalLinkComponent :url="repoUrl" :name="repoUrl" />
</template>
</i18n-t>
</div>
<div class="block">
<div class="buttons is-centered">
<button class="button is-light" @click="toMainView">{{ $t("about.close") }}</button>
</div>
</div>
<h1>{{ $t("about.title") }}</h1>
<h4>{{ $t("about.name") }}</h4>
<i18n-t scope="global" keypath="about.version" tag="p">
<template v-slot:version>{{ version }}</template>
</i18n-t>
<i18n-t scope="global" keypath="about.license" tag="p">
<template v-slot:mit>
<ExternalLinkComponent :url="$t('about.license_mit_url')" name="MIT" />
</template>
<template v-slot:apache>
<ExternalLinkComponent :url="$t('about.license_apache_url')" name="Apache 2.0" />
</template>
</i18n-t>
<i18n-t scope="global" keypath="about.repository" tag="p">
<template v-slot:url>
<ExternalLinkComponent :url="repoUrl" :name="repoUrl" />
</template>
</i18n-t>
<ButtonGroupComponent>
<button type="button" class="btn btn-secondary" @click="toMainView">{{ $t("about.close") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>

View file

@ -5,6 +5,7 @@ import { useStorage } from '@vueuse/core';
import { QrcodeStream, setZXingModuleOverrides } from 'vue-qrcode-reader';
import { sha256 } from '@noble/hashes/sha256';
import base32Encode from 'base32-encode';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
import wasmFile from "../../node_modules/@sec-ant/zxing-wasm/dist/reader/zxing_reader.wasm?url";
@ -131,43 +132,38 @@ const resetErrorMessage = () => {
<template>
<LayoutComponent>
<h1 class="title is-1">{{ $t("addAccount.title") }}</h1>
<div class="notification is-danger is-light" v-if="errorMessageId">
<button class="delete" @click="resetErrorMessage"></button>
<h1>{{ $t("addAccount.title") }}</h1>
<div class="alert alert-danger alert-dismissible fade show" role="alert" v-if="errorMessageId">
{{ $t(errorMessageId) }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" @click="resetErrorMessage"></button>
</div>
<div class="container" id="new-account-error-msg-container"></div>
<div class="field">
<label class="label" for="new-addr-local-part">{{ $t("addAccount.localPart") }}</label>
<div class="control">
<input class="input" type="text" id="new-addr-local-part" v-model="localPart">
<div class="mb-3">
<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">
</div>
<div class="mb-3">
<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">
</div>
<div class="mb-3">
<label class="form-label" for="new-addr-domain">{{ $t("addAccount.domainName") }}</label>
<input class="form-control" type="text" id="new-addr-domain" placeholder="example.org" v-model="domainName">
</div>
<div class="mb-3">
<label class="form-label" for="new-addr-key">{{ $t("addAccount.privateKey") }}</label>
<div class="input-group">
<input class="form-control" type="text" id="new-addr-key" v-model="privateKey">
<button class="btn btn-primary" type="button" @click="showQrCodeScanner">{{ $t("addAccount.scan") }}</button>
</div>
</div>
<div class="field">
<label class="label" for="new-addr-separator">{{ $t("addAccount.separator") }}</label>
<div class="control">
<input class="input" type="text" id="new-addr-separator" v-model="separator">
</div>
</div>
<div class="field">
<label class="label" for="new-addr-domain">{{ $t("addAccount.domainName") }}</label>
<div class="control">
<input class="input" type="text" id="new-addr-domain" placeholder="example.org" v-model="domainName">
</div>
</div>
<label class="label" for="new-addr-key">{{ $t("addAccount.privateKey") }}</label>
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" id="new-addr-key" v-model="privateKey">
</div>
<p class="control">
<a class="button is-primary" @click="showQrCodeScanner">{{ $t("addAccount.scan") }}</a>
</p>
</div>
<qrcode-stream v-if="scanQrCode" @detect="onQrCodeDetected" @error="onQrCodeError"></qrcode-stream>
<div class="buttons is-centered">
<button class="button is-primary" :disabled="addDisabled" @click="addAccount">{{ $t("addAccount.addAccount") }}</button>
<button class="button is-light" v-if="!cancellDisabled" @click="toMainView">{{ $t("addAccount.cancel") }}</button>
</div>
<ButtonGroupComponent>
<button type="button" class="btn btn-primary" :disabled="addDisabled" @click="addAccount">{{ $t("addAccount.addAccount") }}</button>
<button type="button" class="btn btn-secondary" v-if="!cancellDisabled" @click="toMainView">{{ $t("addAccount.cancel") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>

View file

@ -3,6 +3,7 @@ import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStorage } from '@vueuse/core';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
const router = useRouter();
@ -21,19 +22,17 @@ watch(locale, async (newLocale) => {
<template>
<LayoutComponent>
<h1 class="title is-1">{{ $t("config.title") }}</h1>
<div class="field">
<label class="label" for="app-language">{{ $t("config.language") }}</label>
<div class="control">
<div class="select is-fullwidth">
<select id="app-language" v-model="$i18n.locale">
<option v-for="locale_id in $i18n.availableLocales" :key="`locale-${locale_id}`" :value="locale_id">{{ $t("locale_name", 1, { locale: locale_id}) }}</option>
</select>
</div>
</div>
</div>
<div class="buttons is-centered">
<button class="button is-light" @click="toMainView">{{ $t("config.close") }}</button>
<h1>{{ $t("config.title") }}</h1>
<div class="mb-3">
<label class="form-label" for="app-language">{{ $t("config.language") }}</label>
<select class="form-select" id="app-language" v-model="$i18n.locale">
<option v-for="locale_id in $i18n.availableLocales" :key="`locale-${locale_id}`" :value="locale_id">{{ $t("locale_name", 1, { locale: locale_id}) }}</option>
</select>
</div>
<ButtonGroupComponent>
<button type="button" class="btn btn-secondary" @click="toMainView">{{ $t("about.close") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>

View file

@ -1,6 +1,7 @@
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { useStorage } from '@vueuse/core';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
const accounts = useStorage('sake-accounts', []);
@ -19,13 +20,15 @@ const toMainView = () => {
<template>
<LayoutComponent>
<h1 class="title is-1">{{ $t("deleteAccount.title") }}</h1>
<h1>{{ $t("deleteAccount.title") }}</h1>
<p>{{ $t("deleteAccount.account") }}</p>
<p class="has-text-weight-semibold is-size-5">{{ account.localPart }}@{{ account.domain }}</p>
<p>{{ $t("deleteAccount.confirm") }}</p>
<div class="buttons is-centered">
<button class="button is-danger" @click="deleteAccount">{{ $t("deleteAccount.delete") }}</button>
<button class="button is-light" @click="toMainView">{{ $t("deleteAccount.cancel") }}</button>
</div>
<ButtonGroupComponent>
<button type="button" class="btn btn-danger" @click="deleteAccount">{{ $t("deleteAccount.delete") }}</button>
<button type="button" class="btn btn-secondary" @click="toMainView">{{ $t("deleteAccount.cancel") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>

View file

@ -6,6 +6,7 @@ import { useStorage } from '@vueuse/core';
import { hmac } from '@noble/hashes/hmac';
import { sha256 } from '@noble/hashes/sha256';
import base32Encode from 'base32-encode';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
import NavBarComponent from '../components/NavBarComponent.vue';
@ -52,32 +53,28 @@ const resetForm = () => {
<template>
<NavBarComponent />
<LayoutComponent>
<h1 class="title is-1">{{ $t("main.title") }}</h1>
<div class="field">
<label class="label" for="account-name">{{ $t("main.account") }}</label>
<div class="control">
<div class="select is-fullwidth">
<select id="account-name" v-model="selectedAccountId">
<option v-for="account in accounts" :key="account.id" :value="account.id">{{ account.localPart }}@{{ account.domain }}</option>
</select>
</div>
</div>
<h1>{{ $t("main.title") }}</h1>
<div class="mb-3">
<label class="form-label" for="account-name">{{ $t("main.account") }}</label>
<select class="form-select" id="account-name" v-model="selectedAccountId">
<option v-for="account in accounts" :key="account.id" :value="account.id">{{ account.localPart }}@{{ account.domain }}</option>
</select>
</div>
<div class="field">
<label class="label" for="sub-addr-name">{{ $t("main.name") }}</label>
<div class="control">
<input class="input" type="text" id="sub-addr-name" :placeholder="$t('main.input')" v-model="subAddrName">
</div>
<div class="mb-3">
<label class="form-label" for="sub-addr-name">{{ $t("main.name") }}</label>
<input class="form-control" type="text" id="sub-addr-name" :placeholder="$t('main.input')" v-model="subAddrName">
</div>
<div class="field">
<label class="label" for="generated-addr">{{ $t("main.address") }}</label>
<div class="control">
<input class="input" type="text" id="generated-addr" v-model="generatedAddr" disabled>
</div>
</div>
<div class="buttons is-centered">
<button class="button is-primary" @click="copyAddr">{{ $t("main.copy") }}</button>
<button class="button is-light" @click="resetForm">{{ $t("main.reset") }}</button>
<div class="mb-3">
<label class="form-label" for="generated-addr">{{ $t("main.address") }}</label>
<input class="form-control" type="text" id="generated-addr" v-model="generatedAddr" disabled>
</div>
<ButtonGroupComponent>
<button type="button" class="btn btn-primary" @click="copyAddr">{{ $t("main.copy") }}</button>
<button type="button" class="btn btn-secondary" @click="resetForm">{{ $t("main.reset") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>

View file

@ -2,6 +2,7 @@
import { sortAccounts } from '../accounts';
import { RouterLink, useRouter } from 'vue-router';
import { useStorage } from '@vueuse/core';
import ButtonGroupComponent from '../components/ButtonGroupComponent.vue';
import LayoutComponent from '../components/LayoutComponent.vue';
const router = useRouter();
@ -17,25 +18,23 @@ const toMainView = () => {
<template>
<LayoutComponent>
<h1 class="title is-1">{{ $t("manageAccounts.title") }}</h1>
<div class="block">
<table class="table is-fullwidth">
<h1>{{ $t("manageAccounts.title") }}</h1>
<table class="table">
<tbody>
<tr v-for="account in accounts">
<th class="has-text-right is-vcentered">
<th class="text-end align-middle">
{{ account.localPart }}@{{ account.domain }}
</th>
<th>
<button class="button is-danger" @click="deleteAccount(account.id)">{{ $t("manageAccounts.delete") }}</button>
<button type="button" class="btn btn-danger" @click="deleteAccount(account.id)">{{ $t("manageAccounts.delete") }}</button>
</th>
</tr>
</tbody>
</table>
</div>
<div class="block">
<div class="buttons is-centered">
<button class="button is-light" @click="toMainView">{{ $t("manageAccounts.close") }}</button>
</div>
</div>
<ButtonGroupComponent>
<button type="button" class="btn btn-secondary" @click="toMainView">{{ $t("manageAccounts.close") }}</button>
</ButtonGroupComponent>
</LayoutComponent>
</template>