mirror of https://github.com/Lissy93/dashy.git
✨ Adds an email widget for AnonAddy
This commit is contained in:
parent
b96af21bc9
commit
58a085a550
|
@ -17,6 +17,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
|
|||
- [Crypto Price History](#crypto-token-price-history)
|
||||
- [Crypto Wallet Balance](#wallet-balance)
|
||||
- [Code Stats](#code-stats)
|
||||
- [Email Aliases (AnonAddy)](#anonaddy)
|
||||
- [Vulnerability Feed](#vulnerability-feed)
|
||||
- [Exchange Rates](#exchange-rates)
|
||||
- [Public Holidays](#public-holidays)
|
||||
|
@ -379,6 +380,50 @@ Display your coding summary. [Code::Stats](https://codestats.net/) is a free and
|
|||
|
||||
---
|
||||
|
||||
### AnonAddy
|
||||
|
||||
[AnonAddy](https://anonaddy.com/) is a free and open source mail forwarding service. Use it to protect your real email address, by using a different alias for each of your online accounts, and have all emails land in your normal inbox(es). Supports custom domains, email replies, PGP-encryption, multiple recipients and more
|
||||
|
||||
This widget display email addresses / aliases from AnonAddy. Click an email address to copy to clipboard, or use the toggle switch to enable/ disable it. Shows usage stats (bandwidth, used aliases etc), as well as total messages recieved, blocked and sent. Works with both self-hosted and managed instances of AnonAddy.
|
||||
|
||||
<p align="center"><img width="400" src="https://i.ibb.co/ZhfyRdV/anonaddy.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`apiKey`** | `string` | Required | Your AnonAddy API Key / Personal Access Token. You can generate this under [Account Settings](https://app.anonaddy.com/settings)
|
||||
**`hostname`** | `string` | _Optional_ | If your self-hosting AnonAddy, then supply the host name. By default it will use the public hosted instance
|
||||
**`apiVersion`** | `string` | _Optional_ | If you're using an API version that is not version `v1`, then specify it here
|
||||
**`limit`** | `number` | _Optional_ | Limit the number of emails shown per page. Defaults to `10`
|
||||
**`sortBy`** | `string` | _Optional_ | Specify the sort order for email addresses. Defaults to `updated_at`. Can be either: `local_part`, `domain`, `email`, `emails_forwarded`, `emails_blocked`, `emails_replied`, `emails_sent`, `created_at`, `updated_at` or `deleted_at`. Precede with a `-` character to reverse order.
|
||||
**`searchTerm`** | `string` | _Optional_ | A search term to filter results by, will search the email, description and domain
|
||||
**`disableControls`** | `boolean` | _Optional_ | Prevent any changes being made to account through the widget. User will not be able to enable or disable aliases through UI when this option is set
|
||||
**`hideMeta`** | `boolean` | _Optional_ | Don't show account meta info (forward/ block count, quota usage etc)
|
||||
**`hideAliases`** | `boolean` | _Optional_ | Don't show email address / alias list. Will only show account meta info
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: anonaddy
|
||||
options:
|
||||
apiKey: "xxxxxxxxxxxxxxxxxxxxxxxx\
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
limit: 5
|
||||
sortBy: created_at
|
||||
disableControls: true
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟢 Enabled
|
||||
- **Auth**: 🔴 Required
|
||||
- **Price**: 🟠 Free for Self-Hosted / Free Plan available on managed instance or $1/month for premium
|
||||
- **Host**: Self-Hosted or Managed
|
||||
- **Privacy**: _See [AnonAddy Privacy Policy](https://anonaddy.com/privacy/)_
|
||||
|
||||
---
|
||||
|
||||
### Vulnerability Feed
|
||||
|
||||
Keep track of recent security advisories and vulnerabilities, with optional filtering by score, exploits, vendor and product. All fields are optional.
|
||||
|
@ -1181,14 +1226,15 @@ Glances can be launched with the `glances` command. You'll need to run it in web
|
|||
|
||||
##### Options
|
||||
|
||||
All Glance's based widgets require a `hostname`
|
||||
All Glance's based widgets require a `hostname`. All other parameters are optional.
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL to your Glances instance (without a trailing slash)
|
||||
**`hostname`** | `string` | Required | The URL or IP + port to your Glances instance (without a trailing slash)
|
||||
**`username`** | `string` | _Optional_ | If you have setup basic auth on Glances, specify username here (defaults to `glances`)
|
||||
**`password`** | `string` | _Optional_ | If you have setup basic auth on Glances, specify password here. **Note**: since this password is in plaintext, it is important not to reuse it anywhere else
|
||||
**`apiVersion`** | `string` | _Optional_ | Specify an API version, defaults to V `3`. Note that support for older versions is limited
|
||||
**`limit`** | `number` | _Optional_ | For widgets that show a time-series chart, optionally limit the number of data points returned. A higher number will show more historical results, but will take longer to load. A value between 300 - 800 is usually optimal
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟢 Enabled
|
||||
|
|
|
@ -0,0 +1,400 @@
|
|||
<template>
|
||||
<div class="anonaddy-wrapper">
|
||||
<!-- Account Info -->
|
||||
<div class="account-info" v-if="meta && !hideMeta">
|
||||
<PercentageChart title="Mail Stats"
|
||||
:values="[
|
||||
{ label: 'Forwarded', size: meta.forwardCount, color: '#20e253' },
|
||||
{ label: 'Blocked', size: meta.blockedCount, color: '#f80363' },
|
||||
{ label: 'Replies', size: meta.repliesCount, color: '#04e4f4' },
|
||||
{ label: 'Sent', size: meta.sentCount, color: '#f6f000' },
|
||||
]" />
|
||||
<div class="meta-item">
|
||||
<span class="lbl">Bandwidth</span>
|
||||
<span class="val">
|
||||
{{ meta.bandwidth | formatBytes }} out of
|
||||
{{ meta.bandwidthLimit !== 100000000 ? (formatBytes(meta.bandwidthLimit)) : '∞'}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="lbl">Active Domains</span>
|
||||
<span class="val">{{ meta.activeDomains }} out of {{ meta.activeDomainsLimit }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="lbl">Shared Domains</span>
|
||||
<span class="val">{{ meta.sharedDomains }} out of {{ meta.sharedDomainsLimit || '∞'}}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="lbl">Usernames</span>
|
||||
<span class="val">{{ meta.usernamesCount }} out of {{ meta.usernamesLimit || '∞'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Email List -->
|
||||
<div class="email-list" v-if="aliases && !hideAliases">
|
||||
<div class="email-row" v-for="alias in aliases" :key="alias.id">
|
||||
<!-- Email address and status -->
|
||||
<div class="row-1">
|
||||
<Toggle v-if="!disableControls" @change="toggleAlias"
|
||||
:defaultState="alias.active" :id="alias.id" :hideLabels="true" />
|
||||
<span v-if="disableControls"
|
||||
:class="`status ${alias.active ? 'active' : 'inactive'}`">●</span>
|
||||
<div class="address-copy" @click="copyToClipboard(alias.fullEmail)" title="Click to Copy">
|
||||
<span class="txt-email">{{ alias.email }}</span>
|
||||
<span class="txt-at">@</span>
|
||||
<span class="txt-domain">{{ alias.domain }}</span>
|
||||
</div>
|
||||
<ClipboardIcon class="copy-btn"
|
||||
@click="copyToClipboard(alias.fullEmail)"
|
||||
v-tooltip="tooltip('Copy alias to clipboard')"
|
||||
/>
|
||||
</div>
|
||||
<!-- Optional description field -->
|
||||
<div class="row-2" v-if="alias.description">
|
||||
<span class="description">{{ alias.description }}</span>
|
||||
</div>
|
||||
<!-- Num emails sent + received -->
|
||||
<div class="row-3">
|
||||
<span class="lbl">Forwarded</span>
|
||||
<span class="val">{{ alias.forwardCount }}</span>
|
||||
<span class="lbl">Blocked</span>
|
||||
<span class="val">{{ alias.blockedCount }}</span>
|
||||
<span class="lbl">Replied</span>
|
||||
<span class="val">{{ alias.repliesCount }}</span>
|
||||
<span class="lbl">Sent</span>
|
||||
<span class="val">{{ alias.sentCount }}</span>
|
||||
</div>
|
||||
<!-- Date created / updated -->
|
||||
<div class="row-4">
|
||||
<span class="lbl">Created</span>
|
||||
<span class="val as-date">{{ alias.createdAt | formatDate }}</span>
|
||||
<span class="val as-time-ago">{{ alias.createdAt | formatTimeAgo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pagination Page Numbers -->
|
||||
<div class="pagination" v-if="numPages && !hideAliases">
|
||||
<span class="page-num first" @click="goToFirst()">«</span>
|
||||
<span class="page-num" v-if="paginationRange[0] !== 1" @click="goToPrevious()">...</span>
|
||||
<span
|
||||
v-for="pageNum in paginationRange" :key="pageNum"
|
||||
@click="goToPage(pageNum)"
|
||||
:class="`page-num ${pageNum === currentPage ? 'selected' : ''}`"
|
||||
>{{ pageNum }}</span>
|
||||
<span class="page-num" @click="goToNext()"
|
||||
v-if="paginationRange[paginationRange.length - 1] < numPages">...</span>
|
||||
<span class="page-num last" @click="goToLast()">»</span>
|
||||
<p class="page-status">Page {{ currentPage }} of {{ numPages }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Toggle from '@/components/FormElements/Toggle';
|
||||
import PercentageChart from '@/components/Charts/PercentageChart';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
import { timestampToDate, getTimeAgo, convertBytes } from '@/utils/MiscHelpers';
|
||||
import ClipboardIcon from '@/assets/interface-icons/open-clipboard.svg';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {
|
||||
Toggle,
|
||||
PercentageChart,
|
||||
ClipboardIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
aliases: null,
|
||||
meta: null,
|
||||
numPages: null,
|
||||
currentPage: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hostname() {
|
||||
return this.options.hostname || widgetApiEndpoints.anonAddy;
|
||||
},
|
||||
apiVersion() {
|
||||
return this.options.apiVersion || 'v1';
|
||||
},
|
||||
limit() {
|
||||
return this.options.limit || '10';
|
||||
},
|
||||
sortBy() {
|
||||
return this.options.sortBy || 'updated_at';
|
||||
},
|
||||
searchTerm() {
|
||||
return this.options.searchTerm || '';
|
||||
},
|
||||
disableControls() {
|
||||
return this.options.disableControls || false;
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('An apiKey is required');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
hideMeta() {
|
||||
return this.options.hideMeta;
|
||||
},
|
||||
hideAliases() {
|
||||
return this.options.hideAliases;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/api/${this.apiVersion}/aliases?`
|
||||
+ `sort=${this.sortBy}&filter[search]=${this.searchTerm}`
|
||||
+ `&page[number]=${this.currentPage}&page[size]=${this.limit}`;
|
||||
},
|
||||
aliasCountEndpoint() {
|
||||
return `${this.hostname}/api/${this.apiVersion}/aliases?filter[search]=${this.searchTerm}`;
|
||||
},
|
||||
accountInfoEndpoint() {
|
||||
return `${this.hostname}/api/${this.apiVersion}/account-details`;
|
||||
},
|
||||
headers() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
},
|
||||
paginationRange() {
|
||||
const arrOfRange = (start, end) => Array(end - start + 1).fill().map((_, idx) => start + idx);
|
||||
const maxNumbers = this.numPages > 10 ? 10 : this.numPages;
|
||||
if (this.currentPage > maxNumbers) {
|
||||
return arrOfRange(this.currentPage - maxNumbers, this.currentPage);
|
||||
}
|
||||
return arrOfRange(1, maxNumbers);
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
formatDate(timestamp) {
|
||||
return timestampToDate(timestamp);
|
||||
},
|
||||
formatTimeAgo(timestamp) {
|
||||
return getTimeAgo(timestamp);
|
||||
},
|
||||
formatBytes(bytes) {
|
||||
return convertBytes(bytes);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchAccountInfo();
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text);
|
||||
this.$toasted.show('Email address copied to clipboard');
|
||||
},
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint, this.headers).then(this.processData);
|
||||
},
|
||||
fetchAccountInfo() {
|
||||
// Get account info
|
||||
this.makeRequest(this.accountInfoEndpoint, this.headers).then(this.processAccountInfo);
|
||||
// Get number of pages of results (in the most inefficient way possible...)
|
||||
this.makeRequest(this.aliasCountEndpoint, this.headers).then((response) => {
|
||||
this.numPages = Math.floor(response.data.length / this.limit);
|
||||
});
|
||||
},
|
||||
processData(data) {
|
||||
// this.numPages = 14; // data.meta.to;
|
||||
this.currentPage = data.meta.current_page;
|
||||
const aliases = [];
|
||||
data.data.forEach((alias) => {
|
||||
aliases.push({
|
||||
id: alias.id,
|
||||
active: alias.active,
|
||||
domain: alias.domain,
|
||||
email: alias.local_part,
|
||||
recipients: alias.recipients,
|
||||
description: alias.description,
|
||||
forwardCount: alias.emails_forwarded,
|
||||
blockedCount: alias.emails_blocked,
|
||||
repliesCount: alias.emails_replied,
|
||||
sentCount: alias.emails_sent,
|
||||
createdAt: alias.created_at,
|
||||
updatedAt: alias.updated_at,
|
||||
deletedAt: alias.deleted_at,
|
||||
fullEmail: alias.email,
|
||||
});
|
||||
});
|
||||
this.aliases = aliases;
|
||||
},
|
||||
processAccountInfo(data) {
|
||||
const res = data.data;
|
||||
this.meta = {
|
||||
name: data.username || res.from_name,
|
||||
bandwidth: res.bandwidth,
|
||||
bandwidthLimit: res.bandwidth_limit || 100000000,
|
||||
activeDomains: res.active_domain_count,
|
||||
activeDomainsLimit: res.active_domain_limit,
|
||||
sharedDomains: res.active_shared_domain_alias_count,
|
||||
sharedDomainsLimit: res.active_shared_domain_alias_limit,
|
||||
usernamesCount: res.username_count,
|
||||
usernamesLimit: res.username_limit,
|
||||
forwardCount: res.total_emails_forwarded,
|
||||
blockedCount: res.total_emails_blocked,
|
||||
repliesCount: res.total_emails_replied,
|
||||
sentCount: res.total_emails_sent,
|
||||
};
|
||||
},
|
||||
toggleAlias(state, id) {
|
||||
if (this.disableControls) {
|
||||
this.$toasted.show('Error, controls disabled', { className: 'toast-error' });
|
||||
} else {
|
||||
const method = state ? 'POST' : 'DELETE';
|
||||
const path = state ? 'active-aliases' : `active-aliases/${id}`;
|
||||
const body = state ? { id } : {};
|
||||
const endpoint = `${this.hostname}/api/${this.apiVersion}/${path}`;
|
||||
this.makeRequest(endpoint, this.headers, method, body).then(() => {
|
||||
const successMsg = `Alias successfully ${state ? 'enabled' : 'disabled'}`;
|
||||
this.$toasted.show(successMsg, { className: 'toast-success' });
|
||||
});
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.progress.start();
|
||||
this.currentPage = page;
|
||||
this.fetchData();
|
||||
},
|
||||
goToFirst() {
|
||||
this.goToPage(1);
|
||||
},
|
||||
goToLast() {
|
||||
this.goToPage(this.numPages);
|
||||
},
|
||||
goToPrevious() {
|
||||
if (this.currentPage > 1) this.goToPage(this.currentPage - 1);
|
||||
},
|
||||
goToNext() {
|
||||
if (this.currentPage < this.numPages) this.goToPage(this.currentPage + 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
|
||||
.anonaddy-wrapper {
|
||||
.account-info {
|
||||
background: var(--widget-accent-color);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: 0.5rem;
|
||||
.meta-item span {
|
||||
font-size: 0.8rem;
|
||||
margin: 0.25rem 0;
|
||||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
font-family: var(--font-monospace);
|
||||
&.lbl {
|
||||
font-weight: bold;
|
||||
margin-right: 0.25rem;
|
||||
&::after { content: ':'; }
|
||||
}
|
||||
}
|
||||
p.username {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
.email-list {
|
||||
span.lbl {
|
||||
&::after { content: ':'; }
|
||||
}
|
||||
span.val {
|
||||
font-family: var(--font-monospace);
|
||||
margin: 0 0.5rem 0 0.25rem;
|
||||
}
|
||||
.email-row {
|
||||
color: var(--widget-text-color);
|
||||
padding: 0.5rem 0;
|
||||
.row-1 {
|
||||
@extend .svg-button;
|
||||
.address-copy {
|
||||
cursor: copy;
|
||||
display: inline;
|
||||
}
|
||||
span.txt-email {
|
||||
font-weight: bold;
|
||||
}
|
||||
span.txt-at {
|
||||
margin: 0 0.1rem;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
span.status {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
vertical-align: middle;
|
||||
&.active { color: var(--success); }
|
||||
&.inactive { color: var(--danger); }
|
||||
}
|
||||
.copy-btn {
|
||||
float: right;
|
||||
border: none;
|
||||
color: var(--widget-text-color);
|
||||
background: var(--widget-accent-color);
|
||||
}
|
||||
}
|
||||
.row-2 {
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: var(--dimming-factor);
|
||||
span.description {
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
.row-3, .row-4 {
|
||||
font-size: 0.8rem;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
.row-4 {
|
||||
span.as-time-ago {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.row-4 {
|
||||
.as-date { display: none; }
|
||||
.as-time-ago { display: inline; }
|
||||
}
|
||||
}
|
||||
&:not(:last-child) { border-bottom: 1px dashed var(--widget-text-color); }
|
||||
}
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
p.page-status {
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
margin: 0.25rem 0;
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
span.page-num {
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.15rem 0.1rem 0.15rem;
|
||||
margin: 0;
|
||||
color: var(--widget-text-color);
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
color: var(--widget-background-color);
|
||||
background: var(--widget-text-color);
|
||||
border: 1px solid var(--widget-background-color);
|
||||
}
|
||||
&:hover {
|
||||
border: 1px solid var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -18,8 +18,15 @@
|
|||
</div>
|
||||
<!-- Widget -->
|
||||
<div v-else class="widget-wrap">
|
||||
<AnonAddy
|
||||
v-if="widgetType === 'anonaddy'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Apod
|
||||
v-if="widgetType === 'apod'"
|
||||
v-else-if="widgetType === 'apod'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
|
@ -370,6 +377,7 @@ export default {
|
|||
OpenIcon,
|
||||
LoadingAnimation,
|
||||
// Register widget components
|
||||
AnonAddy: () => import('@/components/Widgets/AnonAddy.vue'),
|
||||
Apod: () => import('@/components/Widgets/Apod.vue'),
|
||||
Clock: () => import('@/components/Widgets/Clock.vue'),
|
||||
CodeStats: () => import('@/components/Widgets/CodeStats.vue'),
|
||||
|
|
Loading…
Reference in New Issue